---
title: "Workflows"
description: "How multi-step work runs on ONE: eight step kinds, all compiling to one signal, and the ones that suspend a run until something outside answers."
type: explanation
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/workflows
---
A workflow is a directed graph of steps that a run walks. Every step compiles to
one thing: `signal(receiver, data)`. The step kind is not a separate execution
model — it names which receiver family the step reaches, and whether the run
keeps going or stops and waits. This page is the mental model behind
[the workflow receivers](/docs/receivers) and the run surfaces that use them.

## One compile target

There is no second execution path. A tool call, a skill invocation, an agent
turn and a stage move are the same operation with different payloads: a receiver
name and a JSON body, dispatched the way any other caller dispatches one. So a
workflow can do anything the API can do, and nothing it does is invisible to the
rest of the substrate.

The consequence for a language model driving a workflow is narrow. The graph
holds the sequence; the model's only probabilistic job is picking which workflow
to run and filling its trigger payload. Everything after that executes the same
way every time.

## The eight kinds

`StepKind` is a closed enum. Verified against `packages/sdk/src/schemas.ts`
lines 203-212 on 2026-09-06 — eight members, in this order:

| Kind | What acts | What the run does |
|---|---|---|
| `trigger` | the world | consumes the signal that matched `trigger-source` |
| `tool` | an integration | dispatches a receiver, or `tools:composio` |
| `skill` | a capability | dispatches `skill:run` |
| `agent` | an actor | dispatches `agent:run` |
| `condition` | logic | evaluates the guard inline and routes |
| `human` | a person | suspends |
| `delay` | time | suspends |
| `sell` | a buyer | dispatches `checkout:create`, then suspends |

`gate` is a dead name for `human` and does not appear in the enum.

`tool → skill → agent` is the autonomy ladder, ordered by how much judgment the
step is allowed. A `tool` step is deterministic — it names a receiver and its
arguments and exercises no discretion. A [skill](/docs/skills) is a scoped,
priced capability with a declared input and output. An [agent](/docs/agents)
step hands the work to an actor with open judgment. A workflow composes up into
a skill; a skill never expands back down into a workflow.

## The kind is not the whole binding

Four of the eight kinds resolve no step binding. `trigger`, `condition`,
`human` and `delay` are handled by the executor's own `step.kind` branch
before `stepBinding` is ever consulted: a `condition` step's guard is
evaluated inline by `evalCondition`, and `human` and `delay` park the run. A
park is not silence — after a `human` park the run layer fires a best-effort
`human:notify` to deliver the approval card; it is the park, not the step's
config, that decides to send it. This is why the design names `logic:eval`,
`time:wait` and `human:ask` do not appear in the receiver registry — the repo
states it directly in
`one.ie/web/src/lib/factory/wire-parity.test.ts:164`: "`logic:eval` /
`human:ask` / `mark` will never be registry keys." `human:ask` is the design
name for the run-park surface; `human:notify` is the shipped receiver that posts
the approval card, `human:asked` the event, `human:resolve` the resume.

For the kinds that do dispatch, the receiver comes from the step's **config**,
not from its kind. `stepBinding` reads the config keys in a fixed order and the
first one present wins:

```
receiver   → that receiver, verbatim
composio   → tools:composio
entity     → a typed create/update for that dimension
lifecycle  → entity:tag, with by and source supplied
actorId    → agent:run
skill      → skill:run
```

A step whose config matches none of these is a no-op pass-through: the run
traverses it without side effects rather than failing. The kind is what the
canvas shows and what decides suspension; the config is what decides the call.

## Suspending and resuming

Three kinds stop a run on their kind alone. The run persists a cursor and
waits for something outside itself:

| Kind | Parks as | Resumes on |
|---|---|---|
| `human` | `awaiting` | `human:resolve` |
| `delay` | `awaiting_delay` | a Durable Object alarm |
| `sell` | `awaiting_payment` | `checkout:resume` |

A `human` step resolves with either a decision (`approved` / `rejected`) or a
form payload, and it resolves the same way from every surface — a web card, a
Telegram inline button, a Discord component. A `sell` step mints a checkout
session and parks until the storefront webhook fires `checkout:resume` with the
`runId` and `stepId` it stamped at create time. See [Payments](/docs/payments)
for the rails behind that step.

An `agent` step can park too, but only when the run is driven with the optional
remote-suspend hook; absent that hook it dispatches `agent:run` normally.

A parked step's output lands in the run's cursor, so `$step.<id>.<field>`
resolves downstream exactly as a dispatched step's output does.

## People and agents drive a run with the same verbs

There is no operator API separate from the agent API. Starting, stopping,
resuming and editing a run are four receivers, and a human console and an agent
send them identically:

| Act | Receiver |
|---|---|
| start | `workflow:run` |
| stop / pause | `workflow:stop` |
| resume a human step | `human:resolve` |
| edit the graph | `workflow:apply-diff` |

All four carry the `manage_workflows` authority label. Who may call them is
answered by the authority walk, not by which surface the call arrived on — see
[Authority and access](/docs/authority). `workflow:apply-diff` is the only way a
workflow's shape changes, so every edit is one validated diff that can be
checked before it persists.

## Four things a first workflow gets wrong

These are laws the code enforces, not conventions.

**A stage move is a `tool` step carrying a `lifecycle` config.** `kind:'lifecycle'`
is not in the enum. `stepBinding` reads `config.lifecycle` and dispatches
`entity:tag`, supplying `by` and `source` itself.

**Naming `from` makes a move conditional.** When the entity does not hold the
named `from` tag, `entity:tag` writes a durable `rejected` receipt and throws
`conflict: stage mismatch`. One case is carved out: if the entity already
holds the destination tag, the same move has arrived twice, and the call
returns a no-op rather than throwing. That is a guard, not a bug — it stops a
workflow recording a stage the world never entered — and only a genuine
conflict fails the step and deposits a warn.

**`custom:<event>` is a tracked event name, not a trigger source.** No
`triggerWorkflows()` call site in the platform emits a `custom:*` source, so a
trigger step configured with one never fires on its own. It fires only when
something calls `workflow:trigger` with that exact source by hand.

**A `condition` guard that will not parse fails closed.** The step fails and its
downstream steps poison. It never silently routes `true`.

## Known limits

- **`await` chaining does not close.** A `tool` step bound to `workflow:trigger`
  with `await: true` spawns a child and parks the parent, but `__awaitParent` is
  written by the executor and read nowhere — `settle()` has no parent-resume
  leg, so that parent stays parked. Chain in call mode instead: fire
  `workflow:trigger` and have the consumer declare that source as its
  `trigger_source`. The await path caps chain depth at 3.
- **A `sell` step suspends even when the checkout session could not be minted.**
  The `checkout:create` dispatch is best-effort; a missing key still parks the
  run.
- **`delay` caps at 90 days.** A longer sleep fails the step loudly rather than
  parking.
- **A human park has no global timeout.** The deadline is per-step config; a
  step configured without one waits until `human:resolve` settles it or the
  run is stopped.

## Next

- [Receivers](/docs/receivers) — the catalog every step dispatches into
- [Skills](/docs/skills) — the middle rung of the autonomy ladder
- [Agents](/docs/agents) — the top rung, and what open judgment costs
- [Authority and access](/docs/authority) — who may run, stop and edit