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
| Field | Notes |
|---|---|
id | Stable workflow identifier |
name | Human-readable |
trigger | API, cron, webhook, or event; see Triggers |
nodes / edges | The explicit executable graph: trigger, tool, data, control-flow, wait, approval, and code nodes |
status | active or disabled. Disabled workflows ignore autonomous triggers |
max_credits | Optional hard credit ceiling per run or trigger delivery |
One graph, several starting points
| Source | Use when |
|---|---|
| Visual builder | Create and configure the executable graph directly |
A .workflow.json file | Version and apply the same graph through CLI, API, or MCP |
| Blueprints | Start from a reusable graph pattern in the catalog |
| Chained columns | Single-table, sequential, no branching |
| Durable recipe compatibility | Preserve 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 --jsonworkflows 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 --jsonThe 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> --jsonOnly 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.externalWriteCountis the subset that writes. A step whose stored effect readsexternal_readwhileinternalistrueis Oxygen reading its own workspace data and never left.connectionIds— the exact outside accounts a live run would act through.billableStepFloorwithcreditsPerBillableStepandbillableCreditsFloor— the minimum a run costs. A tool pricedno_billstill 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.
Related
- Triggers — what fires a workflow.
- Modes —
smoke_test,dry_run,live. - Runs — the record of a single fire.
- Blueprints, Durable recipes — defining workflows.