> ## Documentation Index
> Fetch the complete documentation index at: https://sidenet.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a workflow

> When to reach for a workflow instead of an agent, the step types, referencing data between steps, and running one.

An agent decides what to do next on every turn. A workflow does not: it runs
the same steps in the same order every time, and the only thing a model
decides is the content of the steps you gave it. That is the whole reason to
reach for one.

## When a workflow beats an agent

* **The sequence must not vary.** Fetch, compute, write — always in that
  order, never skipping the check in the middle.
* **The computation must be exact.** Sums, groupings and date arithmetic
  belong in code, not in a model's head.
* **Nobody is there to steer.** Scheduled and unattended runs need a
  deterministic path; see [User workflows and schedules](/docs/user-workflows) for
  the end-user variant.

When judgement *is* the point — an open question, an unknown number of tool
calls — keep it in an agent, and hand the agent the workflow as a tool for the
parts that must not vary. See [Giving an agent a workflow](/docs/workflows-as-tools).

## The shape of a definition

A workflow is a JSON document with three parts: `steps`, a map of named
steps; `flow`, the ordered list of operators that run them; and an optional
`output_text` template for the result message.

```json theme={null}
{
  "steps": {
    "fetch":  { "kind": "tool", "tool": { "source": "custom", "id": "TOOL_ID" }, "input": { "since": { "ref": "$.input.since" } } },
    "total":  { "kind": "code", "code": "return { sum: input.rows.reduce((s, r) => s + r.amount, 0) }", "input": { "rows": { "ref": "$.steps.fetch.items" } } },
    "write":  { "kind": "agent", "config": { "model_slug": "openai/gpt-4o-mini", "prompt": "Summarise a day of orders in two sentences." }, "prompt": "Orders since {{ $.input.since }} totalled {{ $.steps.total.sum }}." }
  },
  "flow": [
    { "type": "step", "ref": "fetch" },
    { "type": "step", "ref": "total" },
    { "type": "step", "ref": "write" }
  ],
  "output_text": "{{ $.steps.write.text }}"
}
```

`GET /v1/workflows/definition-schema` returns the full JSON Schema, with a
definition per step kind, if you generate or validate definitions in your own
tooling.

## Step kinds

| `kind`     | What it does                            | Key fields                                                         |
| ---------- | --------------------------------------- | ------------------------------------------------------------------ |
| `tool`     | Calls one tool from your catalogue      | `tool` (a stable reference), `input`, optional `loop`              |
| `agent`    | One model call, configured on the step  | `config` (model, instructions), `prompt`, optional `output_schema` |
| `workflow` | Runs another workflow as a sub-workflow | `ref`, `input`, `version` (`active` by default)                    |
| `map`      | Reshapes data with no model and no call | `mapping`                                                          |
| `code`     | Runs JavaScript                         | `code`, `input`, `timeout_ms`                                      |

**Agent steps are self-contained.** The model, its settings (temperature,
reasoning effort) and its instructions live on the step. An agent step does
not reference one of your saved agents, so editing an agent can never change
what a workflow does, and none of a saved agent's memory, tools or sub-agents
apply. Give it an `output_schema` and the step returns the parsed object
directly; without one it returns `{ text, object, raw }`.

**Tool steps reference tools by stable id**, not by name — a renamed tool or
provider keeps working. A tool step can carry a `loop` block to walk every
page, fan out over a list of values and project fields, exactly as an agent
with `loop_responses` would; see [Working with large APIs](/docs/large-apis#in-workflows).

**Code steps** run in a worker with a blank environment: standard ECMAScript
globals plus `console`, and nothing else — no `fetch`, `require`, `process`,
filesystem or timers. Use a tool step to reach anything outside. The code
receives its `input` mapping as `input`, may use `await`, and must `return` a
JSON-serialisable value, which becomes `$.steps.<id>` with no envelope. The
default timeout is five seconds, the ceiling thirty. In the Studio, the keys
of every `return { … }` literal feed the reference autocomplete for later
steps, so a step that returns `{ sum, count }` offers `$.steps.total.sum` to
whatever comes next.

## Flow operators

`flow` is a list of operators. Each references steps by id.

| `type`        | Behaviour                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------ |
| `step`        | Run one step.                                                                              |
| `parallel`    | Run two or more steps at the same time; the next operator waits for all of them.           |
| `branch`      | Evaluate conditions in order and run the first matching step. No match skips the operator. |
| `foreach`     | Run a step once per item of the previous output, optionally with `concurrency`.            |
| `loop`        | Repeat a step `while` or `until` a condition holds.                                        |
| `sleep`       | Wait a fixed number of milliseconds.                                                       |
| `sleep_until` | Wait until a timestamp resolved at run time; a past time continues immediately.            |

Conditions use a small expression language — `eq`, `ne`, `gt`, `gte`, `lt`,
`lte`, `truthy`, `not`, `and`, `or` — over the same references as everything
else, so a branch can gate on `$.steps.fetch._loop.complete` or on a code
step's result.

Control-flow positions reference a single step. To nest a sequence inside a
branch or a loop, put the sequence in its own workflow and reference it with
a `workflow` step.

### Parallel steps

In the Studio a `parallel` operator is a **gate** node: the previous operator
attaches to the gate, members fan out from it, and the next operator fans in
from all of them. Fill it three ways — drop a step from the palette onto the
gate, connect an existing step node to it, or add members from the gate's
config panel. Later steps read each member's result at `$.steps.<name>` as
usual.

## Wiring data

Every reference is a path rooted at `$`:

| Root                  | Meaning                                                 |
| --------------------- | ------------------------------------------------------- |
| `$.input.<path>`      | The workflow's input, validated against `input_schema`. |
| `$.steps.<id>.<path>` | The output of an earlier step.                          |
| `$.current`           | The current item inside a `foreach` or `loop`.          |

Index arrays with numeric brackets — `$.steps.fetch.items[0].id`. In a step's
`input`, `mapping` or condition, write a reference as `{ "ref": "$.…" }`; in an
agent step's `prompt` and in `output_text`, embed it as `{{ $.… }}`.
Non-string values render as JSON and an unresolved reference renders as an
empty string.

In the Studio, references are picked from a tree rather than typed, and
display without the `$.` prefix (`steps.fetch.items[0].id`); the prefix is
added back when the draft is saved. **Renaming a step rewrites every
reference to it** — in inputs, conditions and prompt templates — so ids are
safe to change as a workflow grows.

Publishing checks the wiring: a reference to a step that runs later, or to a
step that does not exist, is rejected with the offending path named.

## Running one

There are three ways:

1. **From the Studio.** The run panel renders a form from `input_schema` (or
   a JSON editor when there is none) and streams the run onto the canvas —
   each node shows its status live. When a step misbehaves, the first thing
   to open is **Input from last run** under that step's configuration, which
   shows exactly what the step received.
2. **From the API.** `POST /v1/workflows/{id}/run` with `input`. Add
   `"stream": true` for server-sent events — one event per step start and
   result, ending in `run-complete` or `run-error` — or `"notify_thread":
   true` to file the result as a conversation the user can read and reply
   to. Select a version with `?version=active|draft|v3`.
3. **As a tool an agent calls.** See [Giving an agent a workflow](/docs/workflows-as-tools).

```bash theme={null}
curl -X POST "https://api.sidenet.ai/v1/workflows/WORKFLOW_ID/run?version=draft" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "since": "2026-09-09" } }'
```

```json theme={null}
{
  "status": "completed",
  "environment": "draft",
  "version": 0,
  "traceId": "…",
  "result": { "text": "…" },
  "text": "Orders since 2026-09-09 totalled 4,120."
}
```

`text` is the rendered `output_text`. A run of the draft is validated first
and rejected with the issues listed if the draft is not runnable; a failed
run answers `500` with the step error.

## Lifecycle

Workflows use the same draft → publish → activate model as agents. Saving a
draft only checks structure, so half-built graphs save fine. Publishing runs
the full validation — unknown or forward references, orphan steps, cycles,
code that does not compile — and then does a trial build without running
anything. Activating picks which published version serves. See
[Versions, environments and deploying](/docs/versions-and-environments).

`output_text` is the message posted after a successful run: a template over
`$.input` and `$.steps`, up to 4,000 characters, which may contain
`<component id="<stepId>"/>` anchors for rich-UI steps. Without it the result
message is a fixed completion notice with the output attached as JSON. Set
it through the API or the draft editor; it is what a user reads in a result
thread.

## Credentials and billing

A run carries the calling user's stored tool credentials — the same store a
token mint or `PATCH /v1/users/{userId}` writes — and they reach both HTTP
and MCP tool steps. There is no fallback to the organization's credentials:
a step whose provider the user has no access to fails, rather than quietly
running as someone else. With the organization API key a `runtimeAuth` map
in the request body wins over the store, which is how a server-side
integration runs a workflow on a user's behalf.

Every run is one trace, billed to the group named on the request (or the
user's group), gated by the same usage limits as chat, and enriched after the
fact with cost and participants — the tools and agent steps that ran. See
[Traces, cost and billing groups](/docs/observability).
