OXYGENOxygen/ Docs
Execution

Workflows

Durable orchestration over tables, columns, tools, transforms, approvals, and external sync.

A workflow chains columns, provider tool calls, transforms, approvals, table writes, and external sync. It fires from an API call, schedule, webhook, or event. Each fire produces a run.

Shape

FieldNotes
idStable workflow identifier
nameHuman-readable
triggerAPI, cron, webhook, or event; see Triggers
nodes / edgesThe explicit executable graph: trigger, tool, data, control-flow, wait, approval, and code nodes
statusactive or disabled. Disabled workflows ignore autonomous triggers
max_creditsOptional hard credit ceiling per run or trigger delivery

One graph, several starting points

SourceUse when
Visual builderCreate and configure the executable graph directly
A .workflow.json fileVersion and apply the same graph through CLI, API, or MCP
BlueprintsStart from a reusable graph pattern in the catalog
Chained columnsSingle-table, sequential, no branching
Durable recipe compatibilityPreserve imperative TypeScript that cannot yet be represented as explicit nodes

In the web app, the top-right Create workflow action opens one chooser with Blank workflow, Workflow template, Blueprint, and Import definition. Open /workspaces/<workspace-id>/workflows and click that action; the chooser opens on the collection page and does not have a separate public URL. Blank opens the canvas. Workflow template opens the curated single-workflow gallery at /workflows/templates. Blueprint opens the wider bundle gallery at /blueprints for motions that can also install tables, columns, and prompts. Import preflights a portable workflow definition and installs it disabled for review.

New workflows use oxygen-workflows-v2: the graph the UI edits is the graph the worker executes. Historic linear manifests and durable recipes remain runnable, but opaque JavaScript stays on one real compatibility node until you replace it with explicit graph nodes; Oxygen never invents an editable diagram from source.

Authoring

oxygen workflows init --id my-workflow           # scaffold a disabled v2 graph
oxygen workflows schema --subject graph --json   # graph JSON Schema
oxygen workflows lint --file my-workflow.workflow.json --phase draft --json
oxygen workflows apply --file my-workflow.workflow.json --draft --json
oxygen workflows lint --file my-workflow.workflow.json --phase publish --json

workflows lint uses the same workspace-aware readiness contract as Apply, Enable, MCP, and the visual editor. It defaults to --phase publish; use --phase draft to ask whether an incomplete definition is still structurally safe to save. The report returns both canSaveDraft and canPublish, and every fatal issue names its blocking phase plus the exact trigger, node, or field to repair. Lint is read-only: it runs no workflow node, calls no provider, and spends no credits. --approved and --max-credits only validate a candidate autonomous authorization; they grant nothing.

Draft apply stores an inert editable revision without publishing: it costs 0 credits, invokes no graph node, and creates no run. Once publish lint is clean, apply without --draft publishes the revision. Running or enabling the workflow is a separate action with its own approval and credit ceiling. Lifecycle, tagging, revision, webhook, and MCP-exposure subcommands are listed in the CLI reference.

Cron-triggered workflows show a human-readable schedule ("Every 5 minutes", "Daily at 10:00") in list/get output.

The trigger inspector accepts one representative nested JSON payload for API, webhook, and event workflows. Oxygen infers its input schema, keeps the exact author-selected payload as input_schema.examples[0], exposes its nested paths to later mappings, and uses it as the input when Test saves and dry-runs that exact draft revision. Invalid visible JSON blocks Save and Test rather than being silently discarded. Treat the example as revisioned definition data: it is included in workflow exports, so replace sensitive values before saving. A verified webhook delivery may be adopted as the representative payload; rejected deliveries cannot. Webhook and event definitions also expose their idempotency path, while workspace event entries state their producer-owned delivery and deduplication contract. Workspace-event filters are editable typed AND clauses in the trigger inspector; each clause uses eq, neq, exists, or not_exists and is stored in the executable manifest rather than as display-only preset copy. The armed badge is derived from the current published trigger, so a newer draft cannot claim that it—not the older published revision—is live.

Authenticated webhook secrets are shown once on first publish and duplication. To replace one, preview the exact current endpoint first, then approve the rotation:

oxygen workflows webhooks rotate <workflow> --json
oxygen workflows webhooks rotate <workflow> --approved --json

The approved response shows the replacement once. The old sender secret stops working immediately, so update the sending system before dismissing it. Rotation changes only the current published endpoint; it does not publish, enable, or run the draft open in the editor. The same confirmation and one-time handoff are available from the webhook trigger inspector and through MCP.

Authenticated deliveries that started a run can be replayed safely through the trigger inspector, CLI, or MCP:

oxygen workflows webhooks deliveries --workflow-id <workflow-uuid> --outcome ran --json
oxygen workflows webhooks replay <delivery-id> --json

Only a verified outcome=ran delivery is replayable. Replay creates a durable, inspectable dry_run from the exact original immutable revision and payload. It records lineage to the delivery and original run but does not append a synthetic inbound row to webhook delivery history. Reusing the same delivery/request key returns the existing replay run instead of creating another. Replay never resends the webhook, reopens the original run, spends credits, calls a paid provider, performs an external write, or turns historical standing authorization into live authority. As with every non-live run, Oxygen internal reads and oxygen.http_json_request outbound GETs can still execute.

Event payloads, Code, and mapped tool inputs

A new Code node receives only the named inputs declared on that node. Each input is a normal workflow value reference to the trigger or a reachable prior node. Its function returns a JSON object matching the declared closed output schema, so a downstream node can map steps.<node-id>.output.<field> before the workflow has ever run. Code runs as JavaScript in the fixed oxygen-js-v1 sandbox with no network, provider tools, secrets, or ambient workflow context. Historical Code revisions that still use run_source remain inspectable without being silently rewritten.

Mapped inputs list only values guaranteed to exist before the receiving step: later steps, disabled steps, and nodes on only one side of a branch are excluded. Array children remain visible as shape but require an explicit Loop before they can be selected. Each field identifies whether its type came from the endpoint's declared schema, the revision-pinned trigger sample, or an exact observed run. The current workflow may show a short representative value from that run; workspace-wide same-tool hints carry paths and types only, never another workflow's values. Stale evidence, nullability, and declared-versus-observed type drift are shown inline. A direct mapping can store an explicit fail-closed string/number/integer/boolean conversion; an unresolved mapped field fails before the provider call starts.

Test is always a safe dry_run. Deterministic and Oxygen-internal reads may produce useful mapping evidence. An intercepted external provider call returns a stub and deliberately teaches no fields; capturing a real provider response is a separate live, approval-gated capability rather than a hidden side effect of Test.

{
  "trigger": { "type": "event", "source": "crm", "event": "signup", "status": "disabled" },
  "nodes": [
    { "id": "trigger", "kind": "trigger", "name": "Signup", "ui": { "x": 0, "y": 0 } },
    {
      "id": "normalize",
      "kind": "code",
      "name": "Normalize",
      "ui": { "x": 280, "y": 0 },
      "code": {
        "version": 1,
        "language": "javascript",
        "runtime": "oxygen-js-v1",
        "source": "(inputs) => ({ email: String(inputs.email || '').trim().toLowerCase() })",
        "inputs": {
          "email": { "type": "ref", "path": "trigger.input.email" }
        },
        "output_schema": {
          "type": "object",
          "properties": { "email": { "type": "string" } },
          "required": ["email"],
          "additionalProperties": false
        },
        "limits": {
          "timeout_ms": 1000,
          "max_input_bytes": 250000,
          "max_output_bytes": 250000
        }
      }
    },
    {
      "id": "find_contact",
      "kind": "tool",
      "name": "Find contact",
      "ui": { "x": 560, "y": 0 },
      "tool": "hubspot.contacts_search",
      "effect": "external_read",
      "params": { "query": { "type": "ref", "path": "steps.normalize.output.email" } }
    }
  ],
  "edges": [
    { "id": "trigger--normalize", "source": "trigger", "target": "normalize" },
    { "id": "normalize--find_contact", "source": "normalize", "target": "find_contact" }
  ]
}

Discover the exact endpoint through tools search --workflow-eligible, hydrate it with tools get, and copy its canonical workflow_effect. First-party oxygen.* runtime tools can be used as hosted Workflow nodes even when their descriptor says they cannot be called directly through tools run.

Running a workflow

oxygen workflows call <workflow-id> --input-json '{"key":"value"}' --mode smoke_test --json
oxygen workflows call <workflow-id> --input-json '{"key":"value"}' --mode dry_run --json
oxygen workflows call <workflow-id> --input-json '{"key":"value"}' --mode live --approved --max-credits 50 --json
oxygen workflows call <workflow-id> --preview --json
oxygen workflows tail <workflow-run-id> --json

--mode is required for a real call, and not needed with --preview. A live call is refused with approval_required without --approved and with spend_cap_required without --max-credits — unconditionally, whether or not the graph spends anything. call invokes the graph directly without simulating its declared trigger and returns a run id; oxygen workflows run <workflow-run-id> inspects one run, oxygen workflows runs --workflow <workflow-id> and oxygen workflows failures list them, and oxygen workflows cancel <run-id> stops one in flight. All of them return run records.

Seeing what a live run would do, before approving it

oxygen workflows call <workflow-id> --preview --json answers "what am I about to approve" without enqueueing anything: every step that leaves Oxygen, the connected accounts it would act through, and what one run costs. It always describes a live run of that exact saved version, so it takes no --mode, needs no --approved or --max-credits, creates no run record, and spends nothing. It describes the whole graph, so it cannot be combined with --node. On MCP the same call is oxygen_workflows_call with preview: true and no mode.

Read three fields together, because each answers a different question:

  • externalStepCount — steps that leave Oxygen at all, reads included. externalWriteCount is the subset that writes. A step whose stored effect reads external_read while internal is true is Oxygen reading its own workspace data and never left.
  • connectionIds — the exact outside accounts a live run would act through.
  • billableStepFloor with creditsPerBillableStep and billableCreditsFloor — the minimum a run costs. A tool priced no_bill still bills the automation-action meter, and AI or enrichment steps bill per row, so the floor can be well under the real total.

requiresStandingAuthorization means this workflow's trigger can fire without a person present, so going live needs a standing authorization bound to this exact revision. Editing the workflow creates a new revision, which revokes it — the next live delivery is refused until it is granted again. A workflow can also emit events that other workflows subscribe to — see Triggers for the event model and its live gate.

Customizing an existing automation

An existing canonical graph opens in the same visual editor as a newly created workflow. Tool nodes expose their exact endpoint, connection/account, schema-derived configuration, and typed mappings from guaranteed previous steps. Add, insert, reconnect, duplicate, disable, or delete nodes; Save creates a draft revision, while Publish makes that revision authoritative.

The Add node and Change endpoint pickers load the provider inventory without running thousands of access checks. Opening a provider or entering a query checks only the visible, bounded result set against the active workspace. Each row shows whether it is Oxygen-managed, connected by OAuth/BYOK, missing setup, partially configured, needs reconnection, or unavailable; it also shows read/write effect and its BYOK or managed-credit posture. Filter by effect, readiness, or cost. A setup-required endpoint can still be added to a draft, and its repair link opens Connections without discarding the draft, but Publish rechecks and blocks until the connection is healthy—even when the workflow will remain disabled. Enable rechecks again before arming it.

A historic source-backed workflow also opens on that canvas, but its opaque payload or durable recipe remains one real compatibility node. You may compose around, disable, duplicate, or delete that node. To edit its internal behavior, replace it with explicit graph nodes or fork the source — Oxygen does not render a decorative recipe plan as executable structure.

Portable definitions move editable graphs between workspaces. workflows export strips source connection UUIDs and autonomous-trigger approval; workflows import --preflight shows the exact destination account bindings first. Import is create-only and always installs disabled, so inspection never arms a trigger or calls a provider.

Permanently deleting a workflow

Workflow definitions have active and disabled states; there is no definition archive/restore lifecycle. (Restoring an earlier revision is separate append-only version history.) delete is permanent and preview-first: run it without --yes and the preview confirms that no run is still non-terminal and names what will be removed. Repeat with --yes to disarm the workflow and remove its definition, revisions, triggers, and workflow-scoped budget policies. Historical run, usage, and audit records remain inspectable and read-only. Recreating the same slug creates a new workflow identity.

Workflows owned by CRM sync, standing CRM automation, a Table feed, or a scheduled Table column must be removed from that owning surface. The delete response names the exact lifecycle command, preventing a schedule or standing configuration from pointing at a missing workflow.

On this page