# Workflows

> The schedule, loop, filter and delivery around your methods for websites, written by AI from a sentence: a trigger (by hand, a schedule or a webhook), then steps that run methods, run transforms (JavaScript the AI writes), loop, wait, send email and call webhooks (to reach Slack, Zapier, Make, n8n or an ERP, send to their webhook). A method the workflow needs and your organization lacks becomes a pending step, built when you accept it.

Source: https://docs.fous.com/api-reference/workflows
Index: https://docs.fous.com/llms.txt

- `GET /v1/workflows`: List workflows
- `POST /v1/workflows`: Create a workflow
- `POST /v1/workflows/ai-code`: Write a transform’s code
- `GET /v1/workflows/{id}`: Get a workflow
- `PATCH /v1/workflows/{id}`: Update a workflow
- `DELETE /v1/workflows/{id}`: Delete a workflow
- `POST /v1/workflows/{id}/steps/{key}/build`: Build a pending step’s method
- `DELETE /v1/workflows/{id}/steps/{key}`: Remove a step
- `GET /v1/workflows/{id}/runs`: List a workflow’s runs
- `POST /v1/workflows/{id}/runs`: Run a workflow
- `GET /v1/workflows/{id}/runs/{run_id}`: Get a workflow run
- `POST /v1/workflows/{id}/runs/{run_id}/cancel`: Cancel a workflow run
- `POST /v1/workflows/{id}/webhook`: Renew a webhook address
- `POST /v1/workflows/{id}/signing-secret`: Renew the signing secret
- `POST /v1/hooks/{token}`: Start a run from a webhook

## The WorkflowSummary object

- `id` (string, required): wf_ followed by 26 characters.
- `name` (string, required)
- `description` (string)
- `trigger_type` (string, required): One of `manual`, `schedule`, `webhook`.
- `enabled` (boolean, required): Whether its schedule or webhook fires. Every workflow can be run by hand, enabled or not.
- `step_count` (integer, required): Its steps, those inside loops too.
- `methods` (array of string, required): The slugs of the methods its steps run.
- `last_run` (object or null, required)
  - `id` (string)
  - `status` (string)
  - `created_at` (string)
  - `started_at` (string or null)
  - `finished_at` (string or null)
- `cost` (object, required): The price of one run, in credits: the credits field for the method, AI, transform, email and webhook steps outside a For each, and per_item more for each item a For each takes (the most an item costs: a For each inside it counts at its max items). It is the full run: only steps that run and succeed are charged, so a run a filter ends early, or whose steps fail, costs less. per_item is null without a For each.
  - `credits` (integer, required)
  - `per_item` (integer or null, required)
- `next_run_at` (string or null): An enabled schedule: when it runs next.
- `created_at` (string)
- `updated_at` (string)

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "name": "Import duty lookup",
  "description": "Looks up the US import duty for an HTS code and emails the rates.",
  "trigger_type": "manual",
  "enabled": false,
  "step_count": 3,
  "methods": [
    "hts.usitc.gov/get_import_duty"
  ],
  "cost": {
    "credits": 3,
    "per_item": null
  },
  "last_run": {
    "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
    "status": "completed",
    "created_at": "2026-11-10T10:52:08.464Z",
    "started_at": "2026-11-10T10:52:08.465Z",
    "finished_at": "2026-11-10T10:52:15.650Z"
  },
  "next_run_at": null,
  "created_at": "2026-11-10T10:52:08.451Z",
  "updated_at": "2026-11-10T10:52:08.451Z"
}
```

## The Workflow object

Every field of [WorkflowSummary](#the-workflowsummary-object), and the fields below, which are new or read differently.

- `step_count` (integer): Its steps, those inside loops too.
- `methods` (array of string): The slugs of the methods its steps run.
- `last_run` (object or null)
  - `id` (string)
  - `status` (string)
  - `created_at` (string)
  - `started_at` (string or null)
  - `finished_at` (string or null)
- `cost` (object): The price of one run, in credits: the credits field for the method, AI, transform, email and webhook steps outside a For each, and per_item more for each item a For each takes (the most an item costs: a For each inside it counts at its max items). It is the full run: only steps that run and succeed are charged, so a run a filter ends early, or whose steps fail, costs less. per_item is null without a For each.
  - `credits` (integer, required)
  - `per_item` (integer or null, required)
- `definition` (object, required): A trigger, then steps. Text in a method’s input, a loop’s over, an email and a webhook may hold templates: {{trigger.<key>}}, {{steps.<key>.<path>}}, {{loop.item}}, {{loop.index}}, {{run.id}}, {{run.started_at}}, {{now}}, {{previous.steps.<key>.<path>}} (the workflow’s last successful run; null on a first run), with filters ({{steps.search.data.results | length}}). A text that is exactly one template keeps the value’s type; a path that leads nowhere is null. A transform’s code reads the same data as data.trigger, data.steps.<key>, data.loop and data.previous.
  - `trigger` (object, required)
    - `type` (string, required): Every workflow can also be run by hand with POST /v1/workflows/{id}/runs. One of `manual`, `schedule`, `webhook`.
    - `inputs` (array of object): What a run takes, read as {{trigger.<key>}}.
      - `key` (string, required)
      - `label` (string)
      - `type` (string, required): One of `string`, `number`, `boolean`.
      - `required` (boolean)
      - `default` (string or number or boolean or null)
      - `description` (string)
    - `schedule` (object): type schedule only.
      - `frequency` (string, required): minutes: every N minutes; hourly: every N hours; daily, weekly and monthly: at `at`, every N days, weeks or months; weekdays: Monday to Friday at `at`. One of `minutes`, `hourly`, `daily`, `weekdays`, `weekly`, `monthly`.
      - `every` (integer): N. Not used by weekdays: leave it out or send 1.
      - `at` (string): HH:MM, daily, weekdays, weekly and monthly.
      - `day` (integer): monthly: the day of the month, 1 to 31, or -1 for the last day (default 1). A day a month does not have (31 in April, 30 in February) is its last day.
      - `weekday` (integer): weekly: 0 is Sunday.
      - `timezone` (string): An IANA time zone, like Europe/Prague. Default UTC.
      - `input` (object): The input a scheduled run takes.
  - `steps` (array of object, required): Any number of steps, loops nested as deep as needed; the definition is at most 200 KB.
    - `key` (string): Unique in the whole workflow; what templates (steps.<key>) and code (data.steps.<key>) read the step’s output by. Left out, it is made from the name (List invoices: list_invoices).
    - `name` (string): Plain words a person reads.
    - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
    - `method` (string or null): method: the method’s slug; null while its method is pending.
    - `pending` (object): method: a method your organization does not have yet. The workflow cannot run or be turned on while a step is pending: accept building it with POST /v1/workflows/{id}/steps/{key}/build (status building, then the step runs the new method, its input and the later steps wired to it by AI). failed: reason says why the build did not make it; building again tries again.
      - `need` (string, required): What the method does, in plain words.
      - `build_prompt` (string, required): What the builder is asked to make.
      - `site` (string or null): The website to build on (a domain or sign-in host). Without one the step cannot be built (409 site_required) and is listed in incomplete; send site to POST /v1/workflows/{id}/steps/{key}/build.
      - `build_id` (string or null, required): Its build, once accepted.
      - `status` (string, required): One of `proposed`, `building`, `failed`.
      - `reason` (string): failed: why.
    - `input` (JSON value): method: its input, an object; any value may be a template. A required input left empty makes the workflow incomplete until it is filled in. ai: more data its prompt is about, a template or an object of them, sent with the prompt; null (default): the prompt alone. Writing the data’s templates in the prompt itself does the same.
    - `connection` (string or null): method: the conn_ id to run with; null for the site’s default.
    - `on_error` (string): method: stop (default) fails the run; continue records the failure and goes on. One of `stop`, `continue`.
    - `instruction` (string): transform: what the step does, in plain words. Change it and send the code as it was saved (or empty), and the code is written again from it when the workflow is saved; code you changed is kept.
    - `code` (string): transform: the body of function (data, stop) that returns the step’s output (JSON). data is {trigger, steps: {<key>: output}, loop?: {item, index}, previous: the workflow’s last successful run before this one as {run_id, finished_at, trigger, steps: {<key>: output}}, or null on its first run, state: the step’s own memory across runs, an object ({} at first) that the next run starts from as this code leaves it once its run completes (top-level transforms; at most 256 KB; the ids already seen, for “only new items”)}; stop(reason) ends the run quietly, successful, like a filter (inside a loop: skips the rest of that item); prefer a filter step for conditions it can test. It runs in an isolated JavaScript sandbox: no network, files or modules, 2 seconds and 64 MB at most. Sent empty with an instruction, Fous writes it with AI when the workflow is saved; code you write is kept as written.
    - `prompt` (string): ai: what to write or decide, in plain words; templates read the data (a list, an object or a long text a template writes in is sent as the prompt’s data, so the prompt’s own words are never cut). A model answers on every run: it reads about 10,000 tokens (prompt and data; longer data is shortened, and the model is told) and writes up to about 800 words. Output: {text}, or with fields an object with each field’s text. A failed answer (the model declined, ran out of room, or did not answer) fails the step and costs nothing.
    - `fields` (array of string): ai: answer these fields (short text each) instead of one text, read as {{steps.<key>.<field>}}. Default [].
    - `conditions` (array of object): filter: what must hold for the run to go on. value is a template; op one of exists, not_exists (empty: null, "", [] or {}), equals, not_equals, contains, not_contains (text in any case; a list by its items), gt, gte, lt, lte (numbers, a numeric text too; a list by its length); compare is text or a template, for the operators that compare. When the filter does not pass, its record is filtered with output {passed: false, message: "Stopped: …", skipped: the action steps it skipped}.
      - `value` (string, required)
      - `op` (string, required): One of `exists`, `not_exists`, `equals`, `not_equals`, `contains`, `not_contains`, `gt`, `gte`, `lt`, `lte`.
      - `compare` (string)
    - `match` (string): filter: all (default) of its conditions, or any one. One of `all`, `any`.
    - `over` (string): loop: a template that gives a list (a text is split at commas and new lines).
    - `max` (integer): loop: at most this many items (the rest are left out): any number, each item’s action steps costing credits. Left out: 10, and 5 on Free.
    - `steps` (array of object): loop: its steps.
      - `key` (string): Unique in the whole workflow; what templates (steps.<key>) and code (data.steps.<key>) read the step’s output by. Left out, it is made from the name (List invoices: list_invoices).
      - `name` (string): Plain words a person reads.
      - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
      - `method` (string or null): method: the method’s slug; null while its method is pending.
      - `pending` (object): method: a method your organization does not have yet. The workflow cannot run or be turned on while a step is pending: accept building it with POST /v1/workflows/{id}/steps/{key}/build (status building, then the step runs the new method, its input and the later steps wired to it by AI). failed: reason says why the build did not make it; building again tries again.
      - `input` (JSON value): method: its input, an object; any value may be a template. A required input left empty makes the workflow incomplete until it is filled in. ai: more data its prompt is about, a template or an object of them, sent with the prompt; null (default): the prompt alone. Writing the data’s templates in the prompt itself does the same.
      - `connection` (string or null): method: the conn_ id to run with; null for the site’s default.
      - `on_error` (string): method: stop (default) fails the run; continue records the failure and goes on. One of `stop`, `continue`.
      - `instruction` (string): transform: what the step does, in plain words. Change it and send the code as it was saved (or empty), and the code is written again from it when the workflow is saved; code you changed is kept.
      - `code` (string): transform: the body of function (data, stop) that returns the step’s output (JSON). data is {trigger, steps: {<key>: output}, loop?: {item, index}, previous: the workflow’s last successful run before this one as {run_id, finished_at, trigger, steps: {<key>: output}}, or null on its first run, state: the step’s own memory across runs, an object ({} at first) that the next run starts from as this code leaves it once its run completes (top-level transforms; at most 256 KB; the ids already seen, for “only new items”)}; stop(reason) ends the run quietly, successful, like a filter (inside a loop: skips the rest of that item); prefer a filter step for conditions it can test. It runs in an isolated JavaScript sandbox: no network, files or modules, 2 seconds and 64 MB at most. Sent empty with an instruction, Fous writes it with AI when the workflow is saved; code you write is kept as written.
      - `prompt` (string): ai: what to write or decide, in plain words; templates read the data (a list, an object or a long text a template writes in is sent as the prompt’s data, so the prompt’s own words are never cut). A model answers on every run: it reads about 10,000 tokens (prompt and data; longer data is shortened, and the model is told) and writes up to about 800 words. Output: {text}, or with fields an object with each field’s text. A failed answer (the model declined, ran out of room, or did not answer) fails the step and costs nothing.
      - `fields` (array of string): ai: answer these fields (short text each) instead of one text, read as {{steps.<key>.<field>}}. Default [].
      - `conditions` (array of object): filter: what must hold for the run to go on. value is a template; op one of exists, not_exists (empty: null, "", [] or {}), equals, not_equals, contains, not_contains (text in any case; a list by its items), gt, gte, lt, lte (numbers, a numeric text too; a list by its length); compare is text or a template, for the operators that compare. When the filter does not pass, its record is filtered with output {passed: false, message: "Stopped: …", skipped: the action steps it skipped}.
      - `match` (string): filter: all (default) of its conditions, or any one. One of `all`, `any`.
      - `over` (string): loop: a template that gives a list (a text is split at commas and new lines).
      - `max` (integer): loop: at most this many items (the rest are left out): any number, each item’s action steps costing credits. Left out: 10, and 5 on Free.
      - `steps` (array of object): loop: its steps.
      - `seconds` (integer): delay.
      - `to` (string): email: addresses separated by commas; empty: the workflow is saved incomplete until it is filled in. On Free, Starter and Pro, only people in your organization (their sign-in addresses); on Business and Enterprise, anyone. It comes from "<workflow name> via Fous" <workflows@fous.com>, and replies go to the person who made the workflow.
      - `subject` (string): email.
      - `body` (JSON value): email: the plain-text body (line breaks kept). webhook: the JSON to send, a template or an object of them; left out, the previous step’s output.
      - `url` (string): webhook: a public https address; empty: the workflow is saved incomplete until it is filled in. Each POST carries X-Fous-Workflow, X-Fous-Run, X-Fous-Signature (t=<unix seconds>,v1=<HMAC-SHA256 in hex of "<t>.<body>" with the workflow’s signing_secret>) and an Idempotency-Key that is the same on every attempt of the step (receivers dedupe on it: Fous retries on 5xx, 408, 429 and connection failures, and a run taken up again sends what it recorded under it; one that got no answer in time is not sent again), plus the step’s headers.
      - `headers` (array of object): webhook: headers sent with each POST, for a receiver that needs a key or a token (Authorization, X-API-Key). Their values are write-only secrets: sent as value to set one (empty text clears it), left out to keep the saved one; answers and run records never hold them, only a masked hint (null: no value yet, and the workflow is incomplete until it has one). Not Host, Content-Type, Content-Length, Idempotency-Key, Connection, Transfer-Encoding or X-Fous-*.
    - `seconds` (integer): delay.
    - `to` (string): email: addresses separated by commas; empty: the workflow is saved incomplete until it is filled in. On Free, Starter and Pro, only people in your organization (their sign-in addresses); on Business and Enterprise, anyone. It comes from "<workflow name> via Fous" <workflows@fous.com>, and replies go to the person who made the workflow.
    - `subject` (string): email.
    - `body` (JSON value): email: the plain-text body (line breaks kept). webhook: the JSON to send, a template or an object of them; left out, the previous step’s output.
    - `url` (string): webhook: a public https address; empty: the workflow is saved incomplete until it is filled in. Each POST carries X-Fous-Workflow, X-Fous-Run, X-Fous-Signature (t=<unix seconds>,v1=<HMAC-SHA256 in hex of "<t>.<body>" with the workflow’s signing_secret>) and an Idempotency-Key that is the same on every attempt of the step (receivers dedupe on it: Fous retries on 5xx, 408, 429 and connection failures, and a run taken up again sends what it recorded under it; one that got no answer in time is not sent again), plus the step’s headers.
    - `headers` (array of object): webhook: headers sent with each POST, for a receiver that needs a key or a token (Authorization, X-API-Key). Their values are write-only secrets: sent as value to set one (empty text clears it), left out to keep the saved one; answers and run records never hold them, only a masked hint (null: no value yet, and the workflow is incomplete until it has one). Not Host, Content-Type, Content-Length, Idempotency-Key, Connection, Transfer-Encoding or X-Fous-*.
      - `name` (string, required)
      - `value` (string): Sent only: the value to save.
      - `hint` (string or null): The saved value masked (its last 4 characters when long), or null.
- `incomplete` (array of object): Values the workflow still needs before it can run or be turned on: an empty webhook address (field url), an email with no one to send it to (to), a method’s required input left empty (input.<name>). A workflow is saved with them empty.
  - `key` (string, required): The step.
  - `field` (string, required)
  - `message` (string, required): What to do, in plain words.
- `webhook_url` (string or null, required): A webhook workflow: where a POST starts a run. The address is the secret; renew it with POST /v1/workflows/{id}/webhook.
- `signing_secret` (string or null): A workflow with webhook steps: what they sign their calls with (X-Fous-Signature), so the address can check the call is from Fous. New one with POST /v1/workflows/{id}/signing-secret.

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "name": "Import duty lookup",
  "description": "Looks up the US import duty for an HTS code and emails the rates.",
  "trigger_type": "manual",
  "enabled": false,
  "step_count": 3,
  "methods": [
    "hts.usitc.gov/get_import_duty"
  ],
  "cost": {
    "credits": 3,
    "per_item": null
  },
  "last_run": {
    "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
    "status": "completed",
    "created_at": "2026-11-10T10:52:08.464Z",
    "started_at": "2026-11-10T10:52:08.465Z",
    "finished_at": "2026-11-10T10:52:15.650Z"
  },
  "next_run_at": null,
  "created_at": "2026-11-10T10:52:08.451Z",
  "updated_at": "2026-11-10T10:52:08.451Z",
  "definition": {
    "trigger": {
      "type": "manual",
      "inputs": [
        {
          "key": "hts_code",
          "label": "HTS code",
          "type": "string",
          "required": true,
          "default": "6109.10.0012",
          "description": ""
        }
      ]
    },
    "steps": [
      {
        "key": "duty",
        "name": "Look up the import duty",
        "type": "method",
        "method": "hts.usitc.gov/get_import_duty",
        "input": {
          "hts_code": "{{trigger.hts_code}}"
        },
        "connection": null,
        "on_error": "stop"
      },
      {
        "key": "note",
        "name": "Write the rates as a note",
        "type": "transform",
        "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
        "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('This code has no rate.');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || 'none'}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\\n');"
      },
      {
        "key": "send",
        "name": "Email the note",
        "type": "email",
        "to": "ada@example.com",
        "subject": "Import duty for {{trigger.hts_code}}",
        "body": "{{steps.note}}"
      }
    ]
  },
  "webhook_url": null,
  "signing_secret": null,
  "incomplete": []
}
```

## The WorkflowRun object

- `id` (string, required): wfr_ followed by 26 characters.
- `workflow_id` (string, required)
- `status` (string, required): waiting: until resume_at, on a delay, to try a step again, for a method’s repair, or (a scheduled run) for someone to sign an account in again (the step’s waiting says which). A run a filter (or stop()) ended early completed: that step is recorded filtered, saying why. skipped: its schedule came due while the last run had not finished; error says which (code previous_run_open, run_id). canceled: by a person, or the workflow was turned off (its schedule’s and webhook’s runs that had not started, or were waiting to retry a step or for a repair). One of `queued`, `running`, `waiting`, `completed`, `failed`, `canceled`, `skipped`.
- `trigger` (string, required): One of `manual`, `schedule`, `webhook`, `api`.
- `input` (object): What it was given, checked against the trigger’s inputs.
- `error` (object or null): A failed run: the failed step’s error, with step and path.
- `credits` (integer, required): What the run cost, in credits: 1 for each method, AI, transform, email and webhook step that ran and completed (those in a For each once per item). Steps a filter skipped, failed attempts and failed steps cost nothing. A step the organization cannot pay for fails with error.code insufficient_credits, and the run stops there.
- `created_at` (string)
- `started_at` (string or null)
- `finished_at` (string or null)
- `resume_at` (string or null)
- `output` (JSON value): With steps, once the run completed: what the last step that did something gave (a method’s data, any other step’s output). Null before it completed.
- `steps` (array of object): Each step as it ran, in the order it started.
  - `seq` (integer, required)
  - `key` (string, required)
  - `path` (string, required): Where it ran: "send" at the top, "loop.send[2]" in a loop’s third item.
  - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
  - `name` (string)
  - `status` (string, required): waiting: to be tried again, for its method’s repair, or for someone to sign its account in again (waiting says which and until when). filtered: a filter that did not pass, or a transform that called stop(); what came after it in its block did not run. One of `running`, `waiting`, `completed`, `failed`, `filtered`.
  - `input` (JSON value): What it was given, its templates resolved (dropped with the run’s data when it expires: 24 hours or the plan’s run history; the workflow’s latest completed run keeps it). ai: {prompt (as sent, up to 20,000 characters), shortened?: true when long texts or lists in its data were shortened to fit, left_out?: items of its data not shown (the model is told how many)}; filter: {conditions} with the values compared.
  - `attempts` (array of object): Its failed attempts, in order: {attempt, at, error: {code, message}, run_id? (a method step’s run)}.
  - `waiting` (object or null): While it waits (status waiting): reason retry (until: the next try; attempt and of: "attempt 2 of 5"), repair (until: the next look; method; repair_until: when its repair is due, null when not known yet) or connection (a scheduled run whose account needs signing in: it goes on as soon as someone signs in; until: the next look, a day at most; site, connection_id and connect_url say where); since: when it began waiting; message, in plain words.
  - `output` (JSON value): method: {status, data, run_id, error}; ai: {text}, or its fields; transform: what its code returned ({stopped: true, message: "Stopped: …", skipped} when it called stop()); filter: {passed: true}, or {passed: false, message: "Stopped: …", skipped: how many action steps it skipped, which cost nothing}; loop: {count, items: [{status: done or filtered}]} (each item’s outputs are its steps’ records); delay: {seconds}; email: {sent}; webhook: {status, body} (body up to 64 KB).
  - `error` (object or null)
  - `method_run_id` (string or null): method: its run, also in GET /v1/runs.
  - `started_at` (string or null)
  - `finished_at` (string or null)

```json
{
  "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "status": "completed",
  "trigger": "manual",
  "input": {
    "hts_code": "6109.10.0012"
  },
  "error": null,
  "credits": 3,
  "created_at": "2026-11-10T10:52:08.464Z",
  "started_at": "2026-11-10T10:52:08.465Z",
  "finished_at": "2026-11-10T10:52:15.650Z",
  "resume_at": null
}
```

## List workflows

`GET https://api.fous.com/v1/workflows`

Your organization’s workflows, the latest change first, each with its trigger, the methods its steps run and its last run. Free.

### Returns

- `200`: The workflows.

The answer has these fields:

- `workflows` ([array of WorkflowSummary](https://docs.fous.com/api-reference/workflows.md#the-workflowsummary-object), required)

### Example request

```bash
curl 'https://api.fous.com/v1/workflows' \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "workflows": [
    {
      "id": "wf_01m72hccc2f47t2r9qry60m4cq",
      "name": "Import duty lookup",
      "description": "Looks up the US import duty for an HTS code and emails the rates.",
      "trigger_type": "manual",
      "enabled": false,
      "step_count": 3,
      "methods": [
        "hts.usitc.gov/get_import_duty"
      ],
      "cost": {
        "credits": 3,
        "per_item": null
      },
      "last_run": {
        "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
        "status": "completed",
        "created_at": "2026-11-10T10:52:08.464Z",
        "started_at": "2026-11-10T10:52:08.465Z",
        "finished_at": "2026-11-10T10:52:15.650Z"
      },
      "next_run_at": null,
      "created_at": "2026-11-10T10:52:08.451Z",
      "updated_at": "2026-11-10T10:52:08.451Z"
    }
  ]
}
```

## Create a workflow

`POST https://api.fous.com/v1/workflows`

Saves a workflow, from a prompt or from a definition. With {prompt}, AI reads your methods (their inputs and outputs), picks the ones the request needs, wires their inputs, writes the code of its transforms and saves the workflow turned off; ai.summary says what it built. What the request needs that none of your methods can do becomes a pending method step (method null, pending {need, build_prompt}) and is listed in ai.missing: accept building it with POST /v1/workflows/{id}/steps/{key}/build. A value the AI cannot know (a webhook address, an email address, an ID of your own) is left empty and listed in incomplete; the AI builds with sensible defaults rather than asking, and its summary says what is left to fill in. A prompt that asks for no automation ("hi", "what can you do?") saves nothing: the answer is 200 {workflow: null, ai} with ai.reply. When a step needs a website or account the AI cannot know ("our supplier portal", "our bank"), it asks instead: 200 {workflow: null, ai} with ai.question; send the conversation again as messages with the person's answer last. timezone (optional) is the person's, for times like "9am". With {name, definition}, the definition is checked against your methods: every problem answers at once in errors, each at its place (steps[2].input.query); a transform with an instruction and empty code gets its code written. Each method step is an ordinary run. A run costs 1 credit for each step that runs (methods, AI, transforms, emails and webhooks; steps inside For each once per item). The number shown before you run (cost) is the full run; a run a filter ends early costs less. Triggers, For each, filters and delays are free; failed steps cost nothing. A step whose failure can pass (the site or Fous failing for a moment, a rate limit) is tried again after 20 s, 2 min, 10 min and 30 min, 5 attempts in all; a method step whose method Fous is repairing waits for the repair. enabled (default false) turns on a schedule or a webhook, which need a paid plan. With Accept: text/event-stream the answer is Server-Sent Events while the AI writes, each `event: <name>` with `data: <JSON>`: status {text} (a short phase line); reply {text} (the next piece of a reply: append the pieces); question {text, options: [{label, value}], placeholder, allow_other} (the reply asks the person to choose, such as which site; show it as a card, and send the choice as the next message); workflow {name, description}, trigger {trigger} and step {index, step} (emitted as each top-level step is completed, in order; loops are emitted with all nested steps included; keys may still be missing) while it builds or changes the workflow; reset {} (the initial draft failed validation and the AI is restarting: discard earlier streamed events); then done with the same body the JSON answer has (the workflow and ai), or error with the error object. The stream’s HTTP status is 200 like the JSON answer’s.

### Request body

Required, JSON. One of:

#### From a prompt

- `prompt` (string)
- `messages` (array of object)
  - `role` (string, required): One of `user`, `assistant`.
  - `text` (string, required)
- `timezone` (string): An IANA time zone, like Europe/Prague. Default UTC.

#### From a definition

- `name` (string, required)
- `description` (string)
- `definition` (object, required): A trigger, then steps. Text in a method’s input, a loop’s over, an email and a webhook may hold templates: {{trigger.<key>}}, {{steps.<key>.<path>}}, {{loop.item}}, {{loop.index}}, {{run.id}}, {{run.started_at}}, {{now}}, {{previous.steps.<key>.<path>}} (the workflow’s last successful run; null on a first run), with filters ({{steps.search.data.results | length}}). A text that is exactly one template keeps the value’s type; a path that leads nowhere is null. A transform’s code reads the same data as data.trigger, data.steps.<key>, data.loop and data.previous.
  - `trigger` (object, required)
    - `type` (string, required): Every workflow can also be run by hand with POST /v1/workflows/{id}/runs. One of `manual`, `schedule`, `webhook`.
    - `inputs` (array of object): What a run takes, read as {{trigger.<key>}}.
      - `key` (string, required)
      - `label` (string)
      - `type` (string, required): One of `string`, `number`, `boolean`.
      - `required` (boolean)
      - `default` (string or number or boolean or null)
      - `description` (string)
    - `schedule` (object): type schedule only.
      - `frequency` (string, required): minutes: every N minutes; hourly: every N hours; daily, weekly and monthly: at `at`, every N days, weeks or months; weekdays: Monday to Friday at `at`. One of `minutes`, `hourly`, `daily`, `weekdays`, `weekly`, `monthly`.
      - `every` (integer): N. Not used by weekdays: leave it out or send 1.
      - `at` (string): HH:MM, daily, weekdays, weekly and monthly.
      - `day` (integer): monthly: the day of the month, 1 to 31, or -1 for the last day (default 1). A day a month does not have (31 in April, 30 in February) is its last day.
      - `weekday` (integer): weekly: 0 is Sunday.
      - `timezone` (string): An IANA time zone, like Europe/Prague. Default UTC.
      - `input` (object): The input a scheduled run takes.
  - `steps` (array of object, required): Any number of steps, loops nested as deep as needed; the definition is at most 200 KB.
    - `key` (string): Unique in the whole workflow; what templates (steps.<key>) and code (data.steps.<key>) read the step’s output by. Left out, it is made from the name (List invoices: list_invoices).
    - `name` (string): Plain words a person reads.
    - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
    - `method` (string or null): method: the method’s slug; null while its method is pending.
    - `pending` (object): method: a method your organization does not have yet. The workflow cannot run or be turned on while a step is pending: accept building it with POST /v1/workflows/{id}/steps/{key}/build (status building, then the step runs the new method, its input and the later steps wired to it by AI). failed: reason says why the build did not make it; building again tries again.
      - `need` (string, required): What the method does, in plain words.
      - `build_prompt` (string, required): What the builder is asked to make.
      - `site` (string or null): The website to build on (a domain or sign-in host). Without one the step cannot be built (409 site_required) and is listed in incomplete; send site to POST /v1/workflows/{id}/steps/{key}/build.
      - `build_id` (string or null, required): Its build, once accepted.
      - `status` (string, required): One of `proposed`, `building`, `failed`.
      - `reason` (string): failed: why.
    - `input` (JSON value): method: its input, an object; any value may be a template. A required input left empty makes the workflow incomplete until it is filled in. ai: more data its prompt is about, a template or an object of them, sent with the prompt; null (default): the prompt alone. Writing the data’s templates in the prompt itself does the same.
    - `connection` (string or null): method: the conn_ id to run with; null for the site’s default.
    - `on_error` (string): method: stop (default) fails the run; continue records the failure and goes on. One of `stop`, `continue`.
    - `instruction` (string): transform: what the step does, in plain words. Change it and send the code as it was saved (or empty), and the code is written again from it when the workflow is saved; code you changed is kept.
    - `code` (string): transform: the body of function (data, stop) that returns the step’s output (JSON). data is {trigger, steps: {<key>: output}, loop?: {item, index}, previous: the workflow’s last successful run before this one as {run_id, finished_at, trigger, steps: {<key>: output}}, or null on its first run, state: the step’s own memory across runs, an object ({} at first) that the next run starts from as this code leaves it once its run completes (top-level transforms; at most 256 KB; the ids already seen, for “only new items”)}; stop(reason) ends the run quietly, successful, like a filter (inside a loop: skips the rest of that item); prefer a filter step for conditions it can test. It runs in an isolated JavaScript sandbox: no network, files or modules, 2 seconds and 64 MB at most. Sent empty with an instruction, Fous writes it with AI when the workflow is saved; code you write is kept as written.
    - `prompt` (string): ai: what to write or decide, in plain words; templates read the data (a list, an object or a long text a template writes in is sent as the prompt’s data, so the prompt’s own words are never cut). A model answers on every run: it reads about 10,000 tokens (prompt and data; longer data is shortened, and the model is told) and writes up to about 800 words. Output: {text}, or with fields an object with each field’s text. A failed answer (the model declined, ran out of room, or did not answer) fails the step and costs nothing.
    - `fields` (array of string): ai: answer these fields (short text each) instead of one text, read as {{steps.<key>.<field>}}. Default [].
    - `conditions` (array of object): filter: what must hold for the run to go on. value is a template; op one of exists, not_exists (empty: null, "", [] or {}), equals, not_equals, contains, not_contains (text in any case; a list by its items), gt, gte, lt, lte (numbers, a numeric text too; a list by its length); compare is text or a template, for the operators that compare. When the filter does not pass, its record is filtered with output {passed: false, message: "Stopped: …", skipped: the action steps it skipped}.
      - `value` (string, required)
      - `op` (string, required): One of `exists`, `not_exists`, `equals`, `not_equals`, `contains`, `not_contains`, `gt`, `gte`, `lt`, `lte`.
      - `compare` (string)
    - `match` (string): filter: all (default) of its conditions, or any one. One of `all`, `any`.
    - `over` (string): loop: a template that gives a list (a text is split at commas and new lines).
    - `max` (integer): loop: at most this many items (the rest are left out): any number, each item’s action steps costing credits. Left out: 10, and 5 on Free.
    - `steps` (array of object): loop: its steps.
      - `key` (string): Unique in the whole workflow; what templates (steps.<key>) and code (data.steps.<key>) read the step’s output by. Left out, it is made from the name (List invoices: list_invoices).
      - `name` (string): Plain words a person reads.
      - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
      - `method` (string or null): method: the method’s slug; null while its method is pending.
      - `pending` (object): method: a method your organization does not have yet. The workflow cannot run or be turned on while a step is pending: accept building it with POST /v1/workflows/{id}/steps/{key}/build (status building, then the step runs the new method, its input and the later steps wired to it by AI). failed: reason says why the build did not make it; building again tries again.
      - `input` (JSON value): method: its input, an object; any value may be a template. A required input left empty makes the workflow incomplete until it is filled in. ai: more data its prompt is about, a template or an object of them, sent with the prompt; null (default): the prompt alone. Writing the data’s templates in the prompt itself does the same.
      - `connection` (string or null): method: the conn_ id to run with; null for the site’s default.
      - `on_error` (string): method: stop (default) fails the run; continue records the failure and goes on. One of `stop`, `continue`.
      - `instruction` (string): transform: what the step does, in plain words. Change it and send the code as it was saved (or empty), and the code is written again from it when the workflow is saved; code you changed is kept.
      - `code` (string): transform: the body of function (data, stop) that returns the step’s output (JSON). data is {trigger, steps: {<key>: output}, loop?: {item, index}, previous: the workflow’s last successful run before this one as {run_id, finished_at, trigger, steps: {<key>: output}}, or null on its first run, state: the step’s own memory across runs, an object ({} at first) that the next run starts from as this code leaves it once its run completes (top-level transforms; at most 256 KB; the ids already seen, for “only new items”)}; stop(reason) ends the run quietly, successful, like a filter (inside a loop: skips the rest of that item); prefer a filter step for conditions it can test. It runs in an isolated JavaScript sandbox: no network, files or modules, 2 seconds and 64 MB at most. Sent empty with an instruction, Fous writes it with AI when the workflow is saved; code you write is kept as written.
      - `prompt` (string): ai: what to write or decide, in plain words; templates read the data (a list, an object or a long text a template writes in is sent as the prompt’s data, so the prompt’s own words are never cut). A model answers on every run: it reads about 10,000 tokens (prompt and data; longer data is shortened, and the model is told) and writes up to about 800 words. Output: {text}, or with fields an object with each field’s text. A failed answer (the model declined, ran out of room, or did not answer) fails the step and costs nothing.
      - `fields` (array of string): ai: answer these fields (short text each) instead of one text, read as {{steps.<key>.<field>}}. Default [].
      - `conditions` (array of object): filter: what must hold for the run to go on. value is a template; op one of exists, not_exists (empty: null, "", [] or {}), equals, not_equals, contains, not_contains (text in any case; a list by its items), gt, gte, lt, lte (numbers, a numeric text too; a list by its length); compare is text or a template, for the operators that compare. When the filter does not pass, its record is filtered with output {passed: false, message: "Stopped: …", skipped: the action steps it skipped}.
      - `match` (string): filter: all (default) of its conditions, or any one. One of `all`, `any`.
      - `over` (string): loop: a template that gives a list (a text is split at commas and new lines).
      - `max` (integer): loop: at most this many items (the rest are left out): any number, each item’s action steps costing credits. Left out: 10, and 5 on Free.
      - `steps` (array of object): loop: its steps.
      - `seconds` (integer): delay.
      - `to` (string): email: addresses separated by commas; empty: the workflow is saved incomplete until it is filled in. On Free, Starter and Pro, only people in your organization (their sign-in addresses); on Business and Enterprise, anyone. It comes from "<workflow name> via Fous" <workflows@fous.com>, and replies go to the person who made the workflow.
      - `subject` (string): email.
      - `body` (JSON value): email: the plain-text body (line breaks kept). webhook: the JSON to send, a template or an object of them; left out, the previous step’s output.
      - `url` (string): webhook: a public https address; empty: the workflow is saved incomplete until it is filled in. Each POST carries X-Fous-Workflow, X-Fous-Run, X-Fous-Signature (t=<unix seconds>,v1=<HMAC-SHA256 in hex of "<t>.<body>" with the workflow’s signing_secret>) and an Idempotency-Key that is the same on every attempt of the step (receivers dedupe on it: Fous retries on 5xx, 408, 429 and connection failures, and a run taken up again sends what it recorded under it; one that got no answer in time is not sent again), plus the step’s headers.
      - `headers` (array of object): webhook: headers sent with each POST, for a receiver that needs a key or a token (Authorization, X-API-Key). Their values are write-only secrets: sent as value to set one (empty text clears it), left out to keep the saved one; answers and run records never hold them, only a masked hint (null: no value yet, and the workflow is incomplete until it has one). Not Host, Content-Type, Content-Length, Idempotency-Key, Connection, Transfer-Encoding or X-Fous-*.
    - `seconds` (integer): delay.
    - `to` (string): email: addresses separated by commas; empty: the workflow is saved incomplete until it is filled in. On Free, Starter and Pro, only people in your organization (their sign-in addresses); on Business and Enterprise, anyone. It comes from "<workflow name> via Fous" <workflows@fous.com>, and replies go to the person who made the workflow.
    - `subject` (string): email.
    - `body` (JSON value): email: the plain-text body (line breaks kept). webhook: the JSON to send, a template or an object of them; left out, the previous step’s output.
    - `url` (string): webhook: a public https address; empty: the workflow is saved incomplete until it is filled in. Each POST carries X-Fous-Workflow, X-Fous-Run, X-Fous-Signature (t=<unix seconds>,v1=<HMAC-SHA256 in hex of "<t>.<body>" with the workflow’s signing_secret>) and an Idempotency-Key that is the same on every attempt of the step (receivers dedupe on it: Fous retries on 5xx, 408, 429 and connection failures, and a run taken up again sends what it recorded under it; one that got no answer in time is not sent again), plus the step’s headers.
    - `headers` (array of object): webhook: headers sent with each POST, for a receiver that needs a key or a token (Authorization, X-API-Key). Their values are write-only secrets: sent as value to set one (empty text clears it), left out to keep the saved one; answers and run records never hold them, only a masked hint (null: no value yet, and the workflow is incomplete until it has one). Not Host, Content-Type, Content-Length, Idempotency-Key, Connection, Transfer-Encoding or X-Fous-*.
      - `name` (string, required)
      - `value` (string): Sent only: the value to save.
      - `hint` (string or null): The saved value masked (its last 4 characters when long), or null.
- `enabled` (boolean): Whether its schedule or webhook fires. Default false.

### Returns

- `200`: From a prompt, JSON or the event stream: the saved workflow with ai (it is saved turned off), or {workflow: null, ai} when it asks for no automation or the AI asks back; nothing was saved then.
- `201`: Saved from a definition.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The definition (or the body) does not hold: errors lists each problem at its place |
| 402 | `insufficient_credits` | No credits left, and no extra credits on (or capped, or failing to charge); nothing starts |
| 403 | `plan_limit` | Schedules and webhooks come with a paid plan (limit.kind api); from a prompt, also the plan’s AI messages used (limit.kind chat_messages) |
| 409 | `conflict` | reason workflow_incomplete: a value is still to fill in (incomplete) or a step’s method still to build (steps), so the workflow cannot run or be turned on |
| 422 | `invalid_input` | reason ai_failed: the AI’s draft did not hold twice |
| 429 | `rate_limited` | Too many AI messages this minute (limit.kind chat_per_minute) |
| 503 | `service_unavailable` | The AI is unavailable: nothing was saved or changed |

### Example request: From a prompt

```bash
curl 'https://api.fous.com/v1/workflows' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "prompt": "Look up the US import duty for an HTS code I enter on hts.usitc.gov and email me the rates"
  }'
```

With every optional parameter:

```bash
curl 'https://api.fous.com/v1/workflows' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "prompt": "Look up the US import duty for an HTS code I enter on hts.usitc.gov and email me the rates",
    "messages": [
      {
        "role": "user",
        "text": "Which supplier portal do you use?"
      }
    ],
    "timezone": "example"
  }'
```

### Example request: From a definition

```bash
curl 'https://api.fous.com/v1/workflows' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Import duty lookup",
    "definition": {
      "trigger": {
        "type": "manual",
        "inputs": [
          {
            "key": "hts_code",
            "label": "HTS code",
            "type": "string",
            "required": true,
            "default": "6109.10.0012",
            "description": ""
          }
        ]
      },
      "steps": [
        {
          "key": "duty",
          "name": "Look up the import duty",
          "type": "method",
          "method": "hts.usitc.gov/get_import_duty",
          "input": {
            "hts_code": "{{trigger.hts_code}}"
          },
          "connection": null,
          "on_error": "stop"
        },
        {
          "key": "note",
          "name": "Write the rates as a note",
          "type": "transform",
          "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
          "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('\''This code has no rate.'\'');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || '\''none'\''}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\''\\n'\'');"
        },
        {
          "key": "send",
          "name": "Email the note",
          "type": "email",
          "to": "ada@example.com",
          "subject": "Import duty for {{trigger.hts_code}}",
          "body": "{{steps.note}}"
        }
      ]
    }
  }'
```

With every optional parameter:

```bash
curl 'https://api.fous.com/v1/workflows' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Import duty lookup",
    "description": "Looks up the US import duty for an HTS code and emails the rates.",
    "definition": {
      "trigger": {
        "type": "manual",
        "inputs": [
          {
            "key": "hts_code",
            "label": "HTS code",
            "type": "string",
            "required": true,
            "default": "6109.10.0012",
            "description": ""
          }
        ]
      },
      "steps": [
        {
          "key": "duty",
          "name": "Look up the import duty",
          "type": "method",
          "method": "hts.usitc.gov/get_import_duty",
          "input": {
            "hts_code": "{{trigger.hts_code}}"
          },
          "connection": null,
          "on_error": "stop"
        },
        {
          "key": "note",
          "name": "Write the rates as a note",
          "type": "transform",
          "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
          "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('\''This code has no rate.'\'');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || '\''none'\''}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\''\\n'\'');"
        },
        {
          "key": "send",
          "name": "Email the note",
          "type": "email",
          "to": "ada@example.com",
          "subject": "Import duty for {{trigger.hts_code}}",
          "body": "{{steps.note}}"
        }
      ]
    },
    "enabled": false
  }'
```

### Response 201

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "name": "Import duty lookup",
  "description": "Looks up the US import duty for an HTS code and emails the rates.",
  "trigger_type": "manual",
  "enabled": false,
  "step_count": 3,
  "methods": [
    "hts.usitc.gov/get_import_duty"
  ],
  "cost": {
    "credits": 3,
    "per_item": null
  },
  "last_run": {
    "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
    "status": "completed",
    "created_at": "2026-11-10T10:52:08.464Z",
    "started_at": "2026-11-10T10:52:08.465Z",
    "finished_at": "2026-11-10T10:52:15.650Z"
  },
  "next_run_at": null,
  "created_at": "2026-11-10T10:52:08.451Z",
  "updated_at": "2026-11-10T10:52:08.451Z",
  "definition": {
    "trigger": {
      "type": "manual",
      "inputs": [
        {
          "key": "hts_code",
          "label": "HTS code",
          "type": "string",
          "required": true,
          "default": "6109.10.0012",
          "description": ""
        }
      ]
    },
    "steps": [
      {
        "key": "duty",
        "name": "Look up the import duty",
        "type": "method",
        "method": "hts.usitc.gov/get_import_duty",
        "input": {
          "hts_code": "{{trigger.hts_code}}"
        },
        "connection": null,
        "on_error": "stop"
      },
      {
        "key": "note",
        "name": "Write the rates as a note",
        "type": "transform",
        "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
        "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('This code has no rate.');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || 'none'}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\\n');"
      },
      {
        "key": "send",
        "name": "Email the note",
        "type": "email",
        "to": "ada@example.com",
        "subject": "Import duty for {{trigger.hts_code}}",
        "body": "{{steps.note}}"
      }
    ]
  },
  "webhook_url": null,
  "signing_secret": null,
  "incomplete": []
}
```

### Response 200

```json
{
  "workflow": null,
  "ai": {
    "summary": null,
    "missing": [],
    "reply": "I can build this: each day it collects new invoices from your supplier portal, summarizes them and emails finance. Which supplier portal do you use?",
    "question": {
      "text": "Which supplier portal do you use?",
      "options": [
        {
          "label": "SAP Ariba",
          "value": "service.ariba.com"
        },
        {
          "label": "Coupa",
          "value": "supplier.coupahost.com"
        },
        {
          "label": "Basware",
          "value": "portal.basware.com"
        }
      ],
      "placeholder": "Portal address, e.g. portal.example.com",
      "allow_other": true
    }
  }
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "2 problems, first steps[0].input.country: Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code.",
    "next_step": "Fix the request as the message says, then send it again.",
    "errors": [
      {
        "path": "steps[0].input.country",
        "message": "Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code."
      },
      {
        "path": "steps[1].code",
        "message": "The code reads step \"send\", which has not run yet here: only steps before this one can be read."
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Write a transform’s code

`POST https://api.fous.com/v1/workflows/ai-code`

AI writes the JavaScript of one transform from its instruction, given the definition it is in (sent as it is, saved or not): it reads the step’s place, the trigger, the steps before it and their methods’ output schemas. Nothing is saved; saving a definition whose transform has an instruction and empty code does the same.

### Request body

Required, JSON.

- `instruction` (string, required)
- `definition` (object, required): The workflow’s definition.
- `step_key` (string, required): The transform’s key in it.

### Returns

- `200`: The code.

The answer has these fields:

- `code` (string, required)

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The body does not hold, or the definition has no such step |
| 402 | `insufficient_credits` | No credits left, and no extra credits on (or capped, or failing to charge); nothing starts |
| 403 | `plan_limit` | The plan’s AI messages are used (limit.kind chat_messages; window: day or period): Free includes 40 in all and 15 a day, the paid plans more each month |
| 422 | `invalid_input` | The AI’s code did not hold twice |
| 429 | `rate_limited` | Too many AI messages this minute (limit.kind chat_per_minute) |
| 503 | `service_unavailable` | The AI is unavailable: nothing was saved or changed |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/ai-code' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "instruction": "Write the general, special and column 2 rates as a short plain-text note",
    "definition": {
      "trigger": {
        "type": "manual",
        "inputs": [
          {
            "key": "hts_code",
            "label": "HTS code",
            "type": "string",
            "required": true,
            "default": "6109.10.0012",
            "description": ""
          }
        ]
      },
      "steps": [
        {
          "key": "duty",
          "name": "Look up the import duty",
          "type": "method",
          "method": "hts.usitc.gov/get_import_duty",
          "input": {
            "hts_code": "{{trigger.hts_code}}"
          },
          "connection": null,
          "on_error": "stop"
        },
        {
          "key": "note",
          "name": "Write the rates as a note",
          "type": "transform",
          "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
          "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('\''This code has no rate.'\'');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || '\''none'\''}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\''\\n'\'');"
        },
        {
          "key": "send",
          "name": "Email the note",
          "type": "email",
          "to": "ada@example.com",
          "subject": "Import duty for {{trigger.hts_code}}",
          "body": "{{steps.note}}"
        }
      ]
    },
    "step_key": "note"
  }'
```

### Response 200

```json
{
  "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('This code has no rate.');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || 'none'}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\\n');"
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "step_key: The definition has no step \"note\".",
    "errors": [
      {
        "path": "step_key",
        "message": "The definition has no step \"note\"."
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Get a workflow

`GET https://api.fous.com/v1/workflows/{id}`

The workflow with its definition, its webhook’s address, and incomplete: the values still to fill in before it can run. Free. While a step’s method is building (pending.status building), read it again every few seconds: once the build ends, the step runs the new method, or its pending.status is failed with the reason.

### Path parameters

- `id` (string, required)

### Returns

A [Workflow](https://docs.fous.com/api-reference/workflows.md#the-workflow-object) object.

- `200`: The workflow.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | No such workflow |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq' \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "name": "Import duty lookup",
  "description": "Looks up the US import duty for an HTS code and emails the rates.",
  "trigger_type": "manual",
  "enabled": false,
  "step_count": 3,
  "methods": [
    "hts.usitc.gov/get_import_duty"
  ],
  "cost": {
    "credits": 3,
    "per_item": null
  },
  "last_run": {
    "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
    "status": "completed",
    "created_at": "2026-11-10T10:52:08.464Z",
    "started_at": "2026-11-10T10:52:08.465Z",
    "finished_at": "2026-11-10T10:52:15.650Z"
  },
  "next_run_at": null,
  "created_at": "2026-11-10T10:52:08.451Z",
  "updated_at": "2026-11-10T10:52:08.451Z",
  "definition": {
    "trigger": {
      "type": "manual",
      "inputs": [
        {
          "key": "hts_code",
          "label": "HTS code",
          "type": "string",
          "required": true,
          "default": "6109.10.0012",
          "description": ""
        }
      ]
    },
    "steps": [
      {
        "key": "duty",
        "name": "Look up the import duty",
        "type": "method",
        "method": "hts.usitc.gov/get_import_duty",
        "input": {
          "hts_code": "{{trigger.hts_code}}"
        },
        "connection": null,
        "on_error": "stop"
      },
      {
        "key": "note",
        "name": "Write the rates as a note",
        "type": "transform",
        "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
        "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('This code has no rate.');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || 'none'}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\\n');"
      },
      {
        "key": "send",
        "name": "Email the note",
        "type": "email",
        "to": "ada@example.com",
        "subject": "Import duty for {{trigger.hts_code}}",
        "body": "{{steps.note}}"
      }
    ]
  },
  "webhook_url": null,
  "signing_secret": null,
  "incomplete": []
}
```

### Error 404

```json
{
  "error": {
    "code": "not_found",
    "message": "No such workflow.",
    "doc_url": "https://docs.fous.com/guides/errors#not_found",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Update a workflow

`PATCH https://api.fous.com/v1/workflows/{id}`

Changes any of name, description, definition and enabled, or asks the AI to change it: send {prompt} or {messages} (the conversation so far, newest last), with timezone if the workflow names a time. A new definition is checked as on create, and a transform with an instruction and empty code gets its code written, as does one whose instruction changed while its code is still the saved one; a run already going keeps the definition it started with. The AI’s change is saved the same way. With preview: true it is answered but not saved (a name and definition sent with it are the draft it reads); the AI that only answers (a question, or asking one back) changes nothing, and its words are in ai. With Accept: text/event-stream the answer streams while the AI writes (same 200, same body in done).

### Path parameters

- `id` (string, required)

### Request body

Required, JSON.

- `name` (string)
- `description` (string)
- `definition` (object): A trigger, then steps. Text in a method’s input, a loop’s over, an email and a webhook may hold templates: {{trigger.<key>}}, {{steps.<key>.<path>}}, {{loop.item}}, {{loop.index}}, {{run.id}}, {{run.started_at}}, {{now}}, {{previous.steps.<key>.<path>}} (the workflow’s last successful run; null on a first run), with filters ({{steps.search.data.results | length}}). A text that is exactly one template keeps the value’s type; a path that leads nowhere is null. A transform’s code reads the same data as data.trigger, data.steps.<key>, data.loop and data.previous.
  - `trigger` (object, required)
    - `type` (string, required): Every workflow can also be run by hand with POST /v1/workflows/{id}/runs. One of `manual`, `schedule`, `webhook`.
    - `inputs` (array of object): What a run takes, read as {{trigger.<key>}}.
      - `key` (string, required)
      - `label` (string)
      - `type` (string, required): One of `string`, `number`, `boolean`.
      - `required` (boolean)
      - `default` (string or number or boolean or null)
      - `description` (string)
    - `schedule` (object): type schedule only.
      - `frequency` (string, required): minutes: every N minutes; hourly: every N hours; daily, weekly and monthly: at `at`, every N days, weeks or months; weekdays: Monday to Friday at `at`. One of `minutes`, `hourly`, `daily`, `weekdays`, `weekly`, `monthly`.
      - `every` (integer): N. Not used by weekdays: leave it out or send 1.
      - `at` (string): HH:MM, daily, weekdays, weekly and monthly.
      - `day` (integer): monthly: the day of the month, 1 to 31, or -1 for the last day (default 1). A day a month does not have (31 in April, 30 in February) is its last day.
      - `weekday` (integer): weekly: 0 is Sunday.
      - `timezone` (string): An IANA time zone, like Europe/Prague. Default UTC.
      - `input` (object): The input a scheduled run takes.
  - `steps` (array of object, required): Any number of steps, loops nested as deep as needed; the definition is at most 200 KB.
    - `key` (string): Unique in the whole workflow; what templates (steps.<key>) and code (data.steps.<key>) read the step’s output by. Left out, it is made from the name (List invoices: list_invoices).
    - `name` (string): Plain words a person reads.
    - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
    - `method` (string or null): method: the method’s slug; null while its method is pending.
    - `pending` (object): method: a method your organization does not have yet. The workflow cannot run or be turned on while a step is pending: accept building it with POST /v1/workflows/{id}/steps/{key}/build (status building, then the step runs the new method, its input and the later steps wired to it by AI). failed: reason says why the build did not make it; building again tries again.
      - `need` (string, required): What the method does, in plain words.
      - `build_prompt` (string, required): What the builder is asked to make.
      - `site` (string or null): The website to build on (a domain or sign-in host). Without one the step cannot be built (409 site_required) and is listed in incomplete; send site to POST /v1/workflows/{id}/steps/{key}/build.
      - `build_id` (string or null, required): Its build, once accepted.
      - `status` (string, required): One of `proposed`, `building`, `failed`.
      - `reason` (string): failed: why.
    - `input` (JSON value): method: its input, an object; any value may be a template. A required input left empty makes the workflow incomplete until it is filled in. ai: more data its prompt is about, a template or an object of them, sent with the prompt; null (default): the prompt alone. Writing the data’s templates in the prompt itself does the same.
    - `connection` (string or null): method: the conn_ id to run with; null for the site’s default.
    - `on_error` (string): method: stop (default) fails the run; continue records the failure and goes on. One of `stop`, `continue`.
    - `instruction` (string): transform: what the step does, in plain words. Change it and send the code as it was saved (or empty), and the code is written again from it when the workflow is saved; code you changed is kept.
    - `code` (string): transform: the body of function (data, stop) that returns the step’s output (JSON). data is {trigger, steps: {<key>: output}, loop?: {item, index}, previous: the workflow’s last successful run before this one as {run_id, finished_at, trigger, steps: {<key>: output}}, or null on its first run, state: the step’s own memory across runs, an object ({} at first) that the next run starts from as this code leaves it once its run completes (top-level transforms; at most 256 KB; the ids already seen, for “only new items”)}; stop(reason) ends the run quietly, successful, like a filter (inside a loop: skips the rest of that item); prefer a filter step for conditions it can test. It runs in an isolated JavaScript sandbox: no network, files or modules, 2 seconds and 64 MB at most. Sent empty with an instruction, Fous writes it with AI when the workflow is saved; code you write is kept as written.
    - `prompt` (string): ai: what to write or decide, in plain words; templates read the data (a list, an object or a long text a template writes in is sent as the prompt’s data, so the prompt’s own words are never cut). A model answers on every run: it reads about 10,000 tokens (prompt and data; longer data is shortened, and the model is told) and writes up to about 800 words. Output: {text}, or with fields an object with each field’s text. A failed answer (the model declined, ran out of room, or did not answer) fails the step and costs nothing.
    - `fields` (array of string): ai: answer these fields (short text each) instead of one text, read as {{steps.<key>.<field>}}. Default [].
    - `conditions` (array of object): filter: what must hold for the run to go on. value is a template; op one of exists, not_exists (empty: null, "", [] or {}), equals, not_equals, contains, not_contains (text in any case; a list by its items), gt, gte, lt, lte (numbers, a numeric text too; a list by its length); compare is text or a template, for the operators that compare. When the filter does not pass, its record is filtered with output {passed: false, message: "Stopped: …", skipped: the action steps it skipped}.
      - `value` (string, required)
      - `op` (string, required): One of `exists`, `not_exists`, `equals`, `not_equals`, `contains`, `not_contains`, `gt`, `gte`, `lt`, `lte`.
      - `compare` (string)
    - `match` (string): filter: all (default) of its conditions, or any one. One of `all`, `any`.
    - `over` (string): loop: a template that gives a list (a text is split at commas and new lines).
    - `max` (integer): loop: at most this many items (the rest are left out): any number, each item’s action steps costing credits. Left out: 10, and 5 on Free.
    - `steps` (array of object): loop: its steps.
      - `key` (string): Unique in the whole workflow; what templates (steps.<key>) and code (data.steps.<key>) read the step’s output by. Left out, it is made from the name (List invoices: list_invoices).
      - `name` (string): Plain words a person reads.
      - `type` (string, required): One of `method`, `ai`, `transform`, `filter`, `loop`, `delay`, `email`, `webhook`.
      - `method` (string or null): method: the method’s slug; null while its method is pending.
      - `pending` (object): method: a method your organization does not have yet. The workflow cannot run or be turned on while a step is pending: accept building it with POST /v1/workflows/{id}/steps/{key}/build (status building, then the step runs the new method, its input and the later steps wired to it by AI). failed: reason says why the build did not make it; building again tries again.
      - `input` (JSON value): method: its input, an object; any value may be a template. A required input left empty makes the workflow incomplete until it is filled in. ai: more data its prompt is about, a template or an object of them, sent with the prompt; null (default): the prompt alone. Writing the data’s templates in the prompt itself does the same.
      - `connection` (string or null): method: the conn_ id to run with; null for the site’s default.
      - `on_error` (string): method: stop (default) fails the run; continue records the failure and goes on. One of `stop`, `continue`.
      - `instruction` (string): transform: what the step does, in plain words. Change it and send the code as it was saved (or empty), and the code is written again from it when the workflow is saved; code you changed is kept.
      - `code` (string): transform: the body of function (data, stop) that returns the step’s output (JSON). data is {trigger, steps: {<key>: output}, loop?: {item, index}, previous: the workflow’s last successful run before this one as {run_id, finished_at, trigger, steps: {<key>: output}}, or null on its first run, state: the step’s own memory across runs, an object ({} at first) that the next run starts from as this code leaves it once its run completes (top-level transforms; at most 256 KB; the ids already seen, for “only new items”)}; stop(reason) ends the run quietly, successful, like a filter (inside a loop: skips the rest of that item); prefer a filter step for conditions it can test. It runs in an isolated JavaScript sandbox: no network, files or modules, 2 seconds and 64 MB at most. Sent empty with an instruction, Fous writes it with AI when the workflow is saved; code you write is kept as written.
      - `prompt` (string): ai: what to write or decide, in plain words; templates read the data (a list, an object or a long text a template writes in is sent as the prompt’s data, so the prompt’s own words are never cut). A model answers on every run: it reads about 10,000 tokens (prompt and data; longer data is shortened, and the model is told) and writes up to about 800 words. Output: {text}, or with fields an object with each field’s text. A failed answer (the model declined, ran out of room, or did not answer) fails the step and costs nothing.
      - `fields` (array of string): ai: answer these fields (short text each) instead of one text, read as {{steps.<key>.<field>}}. Default [].
      - `conditions` (array of object): filter: what must hold for the run to go on. value is a template; op one of exists, not_exists (empty: null, "", [] or {}), equals, not_equals, contains, not_contains (text in any case; a list by its items), gt, gte, lt, lte (numbers, a numeric text too; a list by its length); compare is text or a template, for the operators that compare. When the filter does not pass, its record is filtered with output {passed: false, message: "Stopped: …", skipped: the action steps it skipped}.
      - `match` (string): filter: all (default) of its conditions, or any one. One of `all`, `any`.
      - `over` (string): loop: a template that gives a list (a text is split at commas and new lines).
      - `max` (integer): loop: at most this many items (the rest are left out): any number, each item’s action steps costing credits. Left out: 10, and 5 on Free.
      - `steps` (array of object): loop: its steps.
      - `seconds` (integer): delay.
      - `to` (string): email: addresses separated by commas; empty: the workflow is saved incomplete until it is filled in. On Free, Starter and Pro, only people in your organization (their sign-in addresses); on Business and Enterprise, anyone. It comes from "<workflow name> via Fous" <workflows@fous.com>, and replies go to the person who made the workflow.
      - `subject` (string): email.
      - `body` (JSON value): email: the plain-text body (line breaks kept). webhook: the JSON to send, a template or an object of them; left out, the previous step’s output.
      - `url` (string): webhook: a public https address; empty: the workflow is saved incomplete until it is filled in. Each POST carries X-Fous-Workflow, X-Fous-Run, X-Fous-Signature (t=<unix seconds>,v1=<HMAC-SHA256 in hex of "<t>.<body>" with the workflow’s signing_secret>) and an Idempotency-Key that is the same on every attempt of the step (receivers dedupe on it: Fous retries on 5xx, 408, 429 and connection failures, and a run taken up again sends what it recorded under it; one that got no answer in time is not sent again), plus the step’s headers.
      - `headers` (array of object): webhook: headers sent with each POST, for a receiver that needs a key or a token (Authorization, X-API-Key). Their values are write-only secrets: sent as value to set one (empty text clears it), left out to keep the saved one; answers and run records never hold them, only a masked hint (null: no value yet, and the workflow is incomplete until it has one). Not Host, Content-Type, Content-Length, Idempotency-Key, Connection, Transfer-Encoding or X-Fous-*.
    - `seconds` (integer): delay.
    - `to` (string): email: addresses separated by commas; empty: the workflow is saved incomplete until it is filled in. On Free, Starter and Pro, only people in your organization (their sign-in addresses); on Business and Enterprise, anyone. It comes from "<workflow name> via Fous" <workflows@fous.com>, and replies go to the person who made the workflow.
    - `subject` (string): email.
    - `body` (JSON value): email: the plain-text body (line breaks kept). webhook: the JSON to send, a template or an object of them; left out, the previous step’s output.
    - `url` (string): webhook: a public https address; empty: the workflow is saved incomplete until it is filled in. Each POST carries X-Fous-Workflow, X-Fous-Run, X-Fous-Signature (t=<unix seconds>,v1=<HMAC-SHA256 in hex of "<t>.<body>" with the workflow’s signing_secret>) and an Idempotency-Key that is the same on every attempt of the step (receivers dedupe on it: Fous retries on 5xx, 408, 429 and connection failures, and a run taken up again sends what it recorded under it; one that got no answer in time is not sent again), plus the step’s headers.
    - `headers` (array of object): webhook: headers sent with each POST, for a receiver that needs a key or a token (Authorization, X-API-Key). Their values are write-only secrets: sent as value to set one (empty text clears it), left out to keep the saved one; answers and run records never hold them, only a masked hint (null: no value yet, and the workflow is incomplete until it has one). Not Host, Content-Type, Content-Length, Idempotency-Key, Connection, Transfer-Encoding or X-Fous-*.
      - `name` (string, required)
      - `value` (string): Sent only: the value to save.
      - `hint` (string or null): The saved value masked (its last 4 characters when long), or null.
- `enabled` (boolean)
- `prompt` (string): A change in plain words, for the AI.
- `messages` (array of object): The conversation so far, newest last, ending with the person’s.
  - `role` (string, required): One of `user`, `assistant`.
  - `text` (string, required)
- `timezone` (string): An IANA time zone. Default UTC.
- `preview` (boolean): With a message to the AI: answer the change without saving it.

### Returns

- `200`: Saved; a change the AI made is the saved workflow with ai (with preview, the workflow as it would be; when the AI only answered, the workflow as it is).

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The definition (or the body) does not hold: errors lists each problem at its place |
| 402 | `insufficient_credits` | No credits left, and no extra credits on (or capped, or failing to charge); nothing starts |
| 403 | `plan_limit` | Schedules and webhooks come with a paid plan (limit.kind api) |
| 404 | `not_found` | No such workflow |
| 409 | `conflict` | reason workflow_incomplete: a value is still to fill in (incomplete) or a step’s method still to build (steps), so the workflow cannot run or be turned on |
| 422 | `invalid_input` | reason ai_failed: the AI’s draft did not hold twice |
| 429 | `rate_limited` | Too many AI messages this minute (limit.kind chat_per_minute) |
| 503 | `service_unavailable` | The AI is unavailable: nothing was saved or changed |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq' \
  -X PATCH \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "enabled": true
  }'
```

With every optional parameter:

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq' \
  -X PATCH \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Import duty lookup",
    "description": "Looks up the US import duty for an HTS code and emails the rates.",
    "definition": {
      "trigger": {
        "type": "manual",
        "inputs": [
          {
            "key": "hts_code",
            "label": "HTS code",
            "type": "string",
            "required": true,
            "default": "6109.10.0012",
            "description": ""
          }
        ]
      },
      "steps": [
        {
          "key": "duty",
          "name": "Look up the import duty",
          "type": "method",
          "method": "hts.usitc.gov/get_import_duty",
          "input": {
            "hts_code": "{{trigger.hts_code}}"
          },
          "connection": null,
          "on_error": "stop"
        },
        {
          "key": "note",
          "name": "Write the rates as a note",
          "type": "transform",
          "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
          "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('\''This code has no rate.'\'');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || '\''none'\''}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\''\\n'\'');"
        },
        {
          "key": "send",
          "name": "Email the note",
          "type": "email",
          "to": "ada@example.com",
          "subject": "Import duty for {{trigger.hts_code}}",
          "body": "{{steps.note}}"
        }
      ]
    },
    "enabled": true,
    "prompt": "Get the US import duty rates for a product by its HTS code",
    "messages": [
      {
        "role": "user",
        "text": "Also return the units of quantity."
      }
    ],
    "timezone": "example",
    "preview": true
  }'
```

### Response 200

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "name": "Import duty lookup",
  "description": "Looks up the US import duty for an HTS code and emails the rates.",
  "trigger_type": "manual",
  "enabled": false,
  "step_count": 3,
  "methods": [
    "hts.usitc.gov/get_import_duty"
  ],
  "cost": {
    "credits": 3,
    "per_item": null
  },
  "last_run": {
    "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
    "status": "completed",
    "created_at": "2026-11-10T10:52:08.464Z",
    "started_at": "2026-11-10T10:52:08.465Z",
    "finished_at": "2026-11-10T10:52:15.650Z"
  },
  "next_run_at": null,
  "created_at": "2026-11-10T10:52:08.451Z",
  "updated_at": "2026-11-10T10:52:08.451Z",
  "definition": {
    "trigger": {
      "type": "manual",
      "inputs": [
        {
          "key": "hts_code",
          "label": "HTS code",
          "type": "string",
          "required": true,
          "default": "6109.10.0012",
          "description": ""
        }
      ]
    },
    "steps": [
      {
        "key": "duty",
        "name": "Look up the import duty",
        "type": "method",
        "method": "hts.usitc.gov/get_import_duty",
        "input": {
          "hts_code": "{{trigger.hts_code}}"
        },
        "connection": null,
        "on_error": "stop"
      },
      {
        "key": "note",
        "name": "Write the rates as a note",
        "type": "transform",
        "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
        "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('This code has no rate.');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || 'none'}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\\n');"
      },
      {
        "key": "send",
        "name": "Email the note",
        "type": "email",
        "to": "ada@example.com",
        "subject": "Import duty for {{trigger.hts_code}}",
        "body": "{{steps.note}}"
      }
    ]
  },
  "webhook_url": null,
  "signing_secret": null,
  "incomplete": [],
  "ai": {
    "summary": "The note now also lists the units of quantity, after the column 2 rate.",
    "missing": [],
    "reply": null,
    "question": null
  }
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "2 problems, first steps[0].input.country: Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code.",
    "next_step": "Fix the request as the message says, then send it again.",
    "errors": [
      {
        "path": "steps[0].input.country",
        "message": "Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code."
      },
      {
        "path": "steps[1].code",
        "message": "The code reads step \"send\", which has not run yet here: only steps before this one can be read."
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Delete a workflow

`DELETE https://api.fous.com/v1/workflows/{id}`

Deletes the workflow: its schedule and webhook stop, and runs still going are canceled. Its runs stay readable.

### Path parameters

- `id` (string, required)

### Returns

- `200`: Deleted.

The answer has these fields:

- `id` (string)
- `deleted` (boolean)

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | No such workflow |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq' \
  -X DELETE \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "deleted": true
}
```

### Error 404

```json
{
  "error": {
    "code": "not_found",
    "message": "No such workflow.",
    "doc_url": "https://docs.fous.com/guides/errors#not_found",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Build a pending step’s method

`POST https://api.fous.com/v1/workflows/{id}/steps/{key}/build`

Starts a build of the method a pending step needs, from the step’s build_prompt, as POST /v1/builds starts one (same plan rules and errors). The step’s pending becomes {build_id, status: building}. When the build ends, the step runs the new method with its input and later steps’ references wired by AI, or pending.status is failed with the reason; calling this again retries. Poll GET /v1/workflows/{id} to follow it. site (optional) sets the step’s site and starts the build in one call, saved only when the build starts. A step already building returns its build; sending a different site for it returns 409.

### Path parameters

- `id` (string, required)
- `key` (string, required)

### Request body

Optional, JSON.

- `site` (string): Optional: the website to build on, like portal.example.com. Replaces the step’s site when the build starts. Required when the step has none (reason site_required).
- `connections` (array of string): Optional: the connected accounts (conn_…) the build may use, as in POST /v1/builds.

### Returns

- `202`: Building.

The answer has these fields:

- `build_id` (string, required): Also in GET /v1/builds/{id}.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The definition (or the body) does not hold: errors lists each problem at its place |
| 403 | `plan_limit` | A plan limit (plan_limit): the plan’s sites all have methods (kind sites), or Free’s 3 builds are used (kind builds); or the caller is a reader (forbidden) |
| 404 | `not_found` | No such workflow, or no step with that key waiting for a method |
| 409 | `conflict` | reason site_required: no site was sent and the step has none. Or code conflict: the step is already building on a different site |
| 422 | `not_buildable`, `request_refused` | No website, app, account or API is involved (not_buildable), or the request check turned it away (request_refused, with its verdict as reason) |
| 429 | `rate_limited` | Another build of the organization is running (limit.kind concurrent_builds) |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/steps/rates/build' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "site": "portal.example.com",
    "connections": [
      "conn_01m5kyvsdxea78rpr2j3qzzd1z"
    ]
  }'
```

### Response 202

```json
{
  "build_id": "bld_01m6vqpmggfbbv1166jvvvmsp7"
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "2 problems, first steps[0].input.country: Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code.",
    "next_step": "Fix the request as the message says, then send it again.",
    "errors": [
      {
        "path": "steps[0].input.country",
        "message": "Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code."
      },
      {
        "path": "steps[1].code",
        "message": "The code reads step \"send\", which has not run yet here: only steps before this one can be read."
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Remove a step

`DELETE https://api.fous.com/v1/workflows/{id}/steps/{key}`

Removes a step, a pending one included, and what is inside it. Whatever read its output reads nothing: templates are emptied, an emptied required text is left to fill in (incomplete), a filter drops conditions about it, and a transform whose code read it gets new code (a transform with code and no instruction is left as it is and the removal answers 400 naming it: change its code first). Saved like any change: an enabled workflow left with a value to fill in is refused (reason workflow_incomplete).

### Path parameters

- `id` (string, required)
- `key` (string, required)

### Returns

A [Workflow](https://docs.fous.com/api-reference/workflows.md#the-workflow-object) object.

- `200`: Removed.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The definition (or the body) does not hold: errors lists each problem at its place |
| 404 | `not_found` | No such workflow or step |
| 409 | `conflict` | reason workflow_incomplete: a value is still to fill in (incomplete) or a step’s method still to build (steps), so the workflow cannot run or be turned on |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/steps/rates' \
  -X DELETE \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "name": "Import duty lookup",
  "description": "Looks up the US import duty for an HTS code and emails the rates.",
  "trigger_type": "manual",
  "enabled": false,
  "step_count": 3,
  "methods": [
    "hts.usitc.gov/get_import_duty"
  ],
  "cost": {
    "credits": 3,
    "per_item": null
  },
  "last_run": {
    "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
    "status": "completed",
    "created_at": "2026-11-10T10:52:08.464Z",
    "started_at": "2026-11-10T10:52:08.465Z",
    "finished_at": "2026-11-10T10:52:15.650Z"
  },
  "next_run_at": null,
  "created_at": "2026-11-10T10:52:08.451Z",
  "updated_at": "2026-11-10T10:52:08.451Z",
  "definition": {
    "trigger": {
      "type": "manual",
      "inputs": [
        {
          "key": "hts_code",
          "label": "HTS code",
          "type": "string",
          "required": true,
          "default": "6109.10.0012",
          "description": ""
        }
      ]
    },
    "steps": [
      {
        "key": "duty",
        "name": "Look up the import duty",
        "type": "method",
        "method": "hts.usitc.gov/get_import_duty",
        "input": {
          "hts_code": "{{trigger.hts_code}}"
        },
        "connection": null,
        "on_error": "stop"
      },
      {
        "key": "note",
        "name": "Write the rates as a note",
        "type": "transform",
        "instruction": "Write the general, special and column 2 rates as a short plain-text note; stop when the code has no rate.",
        "code": "const duty = data.steps.duty.data;\nif (!duty || !duty.general_rate) stop('This code has no rate.');\nreturn [\n  `HTS ${duty.hts_code} (${duty.release})`,\n  `General rate: ${duty.general_rate}`,\n  `Special rates: ${duty.special_rates || 'none'}`,\n  `Column 2 rate: ${duty.column_2_rate}`,\n  duty.source_url,\n].join('\\n');"
      },
      {
        "key": "send",
        "name": "Email the note",
        "type": "email",
        "to": "ada@example.com",
        "subject": "Import duty for {{trigger.hts_code}}",
        "body": "{{steps.note}}"
      }
    ]
  },
  "webhook_url": null,
  "signing_secret": null,
  "incomplete": []
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "2 problems, first steps[0].input.country: Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code.",
    "next_step": "Fix the request as the message says, then send it again.",
    "errors": [
      {
        "path": "steps[0].input.country",
        "message": "Not an input of hts.usitc.gov/get_import_duty. It takes: hts_code."
      },
      {
        "path": "steps[1].code",
        "message": "The code reads step \"send\", which has not run yet here: only steps before this one can be read."
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## List a workflow’s runs

`GET https://api.fous.com/v1/workflows/{id}/runs`

The workflow’s runs, newest first, without their steps. Free.

### Path parameters

- `id` (string, required)

### Query parameters

- `limit` (integer): Default 20.
- `before` (string): The id of the last run you read (wfr_…), or an ISO time: the page after it. Keep reading while has_more is true.

### Returns

- `200`: The runs.

The answer has these fields:

- `runs` ([array of WorkflowRun](https://docs.fous.com/api-reference/workflows.md#the-workflowrun-object), required)
- `has_more` (boolean, required): Whether older runs follow.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | No such workflow |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/runs' \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

With every optional parameter:

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/runs?limit=20&before=wfr_01m72hcccgfzqsgbfn5w7q33sv' \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "runs": [
    {
      "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
      "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
      "status": "completed",
      "trigger": "manual",
      "input": {
        "hts_code": "6109.10.0012"
      },
      "error": null,
      "credits": 3,
      "created_at": "2026-11-10T10:52:08.464Z",
      "started_at": "2026-11-10T10:52:08.465Z",
      "finished_at": "2026-11-10T10:52:15.650Z",
      "resume_at": null
    }
  ],
  "has_more": false
}
```

### Error 404

```json
{
  "error": {
    "code": "not_found",
    "message": "No such workflow.",
    "doc_url": "https://docs.fous.com/guides/errors#not_found",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Run a workflow

`POST https://api.fous.com/v1/workflows/{id}/runs`

Starts a run by hand, enabled or not, with input for the trigger’s inputs (defaults filled in, numbers and booleans read from text). With wait (0 to 60 seconds, default 0) the answer is held until the run ends: 200 once it ended, else 202 while it goes on; read it with GET /v1/workflows/{id}/runs/{run_id}. Each method step is a run of its own, in GET /v1/runs too. A run costs 1 credit for each step that runs (methods, AI, transforms, emails and webhooks; steps inside For each once per item). The number shown before you run (cost) is the full run; a run a filter ends early costs less. Triggers, For each, filters and delays are free; failed steps cost nothing. A step whose failure can pass (the site or Fous failing for a moment, a rate limit) is tried again after 20 s, 2 min, 10 min and 30 min, 5 attempts in all; a method step whose method Fous is repairing waits for the repair. A step the organization cannot pay for fails with error.code insufficient_credits, and the run stops there.

### Path parameters

- `id` (string, required)

### Request body

Optional, JSON.

- `input` (object): Values for the trigger’s inputs.
- `wait` (integer): Default 0.

### Returns

A [WorkflowRun](https://docs.fous.com/api-reference/workflows.md#the-workflowrun-object) object.

- `200`: The run ended within wait.
- `202`: Queued or still going. The same WorkflowRun, with status "queued".

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The body does not hold, or the input does not fit the trigger’s inputs |
| 404 | `not_found` | No such workflow |
| 409 | `conflict` | reason workflow_incomplete: a value is still to fill in (incomplete) or a step’s method still to build (steps), so the workflow cannot run or be turned on |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/runs' \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "input": {
      "hts_code": "6109.10.0012"
    },
    "wait": 30
  }'
```

### Response 200

```json
{
  "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "status": "completed",
  "trigger": "manual",
  "input": {
    "hts_code": "6109.10.0012"
  },
  "error": null,
  "credits": 3,
  "output": {
    "sent": 1
  },
  "created_at": "2026-11-10T10:52:08.464Z",
  "started_at": "2026-11-10T10:52:08.465Z",
  "finished_at": "2026-11-10T10:52:15.650Z",
  "resume_at": null,
  "steps": [
    {
      "seq": 1,
      "key": "duty",
      "path": "duty",
      "type": "method",
      "name": "Look up the import duty",
      "status": "completed",
      "input": {
        "hts_code": "6109.10.0012"
      },
      "output": {
        "status": "completed",
        "data": {
          "release": "2026 HTS Revision 22",
          "hts_code": "6109.10.00.12",
          "description": "Men's (338)",
          "description_path": [
            "T-shirts, singlets, tank tops and similar garments, knitted or crocheted:",
            "Of cotton",
            "Men's or boys':",
            "Other T-shirts:",
            "Men's (338)"
          ],
          "general_rate": "16.5%",
          "special_rates": "Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)",
          "column_2_rate": "90%",
          "rate_source_code": "6109.10.00",
          "source_url": "https://hts.usitc.gov/search?query=6109100012",
          "units": [
            "doz.",
            "kg"
          ],
          "footnotes": [],
          "note": "Rates are those shown in the current HTS schedule; additional duties and eligibility conditions may apply."
        },
        "run_id": "run_01m72hcccpfjaatbegf499rgk8",
        "error": null
      },
      "error": null,
      "method_run_id": "run_01m72hcccpfjaatbegf499rgk8",
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:08.470Z",
      "finished_at": "2026-11-10T10:52:15.291Z"
    },
    {
      "seq": 2,
      "key": "note",
      "path": "note",
      "type": "transform",
      "name": "Write the rates as a note",
      "status": "completed",
      "input": null,
      "output": "HTS 6109.10.00.12 (2026 HTS Revision 22)\nGeneral rate: 16.5%\nSpecial rates: Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)\nColumn 2 rate: 90%\nhttps://hts.usitc.gov/search?query=6109100012",
      "error": null,
      "method_run_id": null,
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:15.293Z",
      "finished_at": "2026-11-10T10:52:15.307Z"
    },
    {
      "seq": 3,
      "key": "send",
      "path": "send",
      "type": "email",
      "name": "Email the note",
      "status": "completed",
      "input": {
        "to": [
          "ada@example.com"
        ],
        "subject": "Import duty for 6109.10.0012",
        "body": "HTS 6109.10.00.12 (2026 HTS Revision 22)\nGeneral rate: 16.5%\nSpecial rates: Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)\nColumn 2 rate: 90%\nhttps://hts.usitc.gov/search?query=6109100012"
      },
      "output": {
        "sent": 1
      },
      "error": null,
      "method_run_id": null,
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:15.310Z",
      "finished_at": "2026-11-10T10:52:15.648Z"
    }
  ]
}
```

### Response 202

```json
{
  "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "status": "queued",
  "trigger": "manual",
  "input": {
    "hts_code": "6109.10.0012"
  },
  "error": null,
  "credits": 0,
  "output": {
    "sent": 1
  },
  "created_at": "2026-11-10T10:52:08.464Z",
  "started_at": null,
  "finished_at": null,
  "resume_at": null,
  "steps": []
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "body: Unrecognized key: \"hts_code\"",
    "errors": [
      {
        "path": "body",
        "message": "Unrecognized key: \"hts_code\""
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Get a workflow run

`GET https://api.fous.com/v1/workflows/{id}/runs/{run_id}`

The run with each step as it ran: its status, resolved input, output and error. A step that takes time (a method, a loop, a delay) shows running until it ends. With wait (0 to 60 seconds, default 0) the answer is held until the run ends. Free.

### Path parameters

- `id` (string, required)
- `run_id` (string, required)

### Query parameters

- `wait` (integer): Seconds to hold the answer while it is still running, 0 to 60. Default 0.

### Returns

A [WorkflowRun](https://docs.fous.com/api-reference/workflows.md#the-workflowrun-object) object.

- `200`: The run has ended.
- `202`: Still going (queued, running or waiting). The same WorkflowRun, with status "running".

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | wait is not a whole number from 0 to 60 |
| 404 | `not_found` | No such workflow run |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/runs/wfr_01m72hcccgfzqsgbfn5w7q33sv' \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

With every optional parameter:

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/runs/wfr_01m72hcccgfzqsgbfn5w7q33sv?wait=30' \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "status": "completed",
  "trigger": "manual",
  "input": {
    "hts_code": "6109.10.0012"
  },
  "error": null,
  "credits": 3,
  "output": {
    "sent": 1
  },
  "created_at": "2026-11-10T10:52:08.464Z",
  "started_at": "2026-11-10T10:52:08.465Z",
  "finished_at": "2026-11-10T10:52:15.650Z",
  "resume_at": null,
  "steps": [
    {
      "seq": 1,
      "key": "duty",
      "path": "duty",
      "type": "method",
      "name": "Look up the import duty",
      "status": "completed",
      "input": {
        "hts_code": "6109.10.0012"
      },
      "output": {
        "status": "completed",
        "data": {
          "release": "2026 HTS Revision 22",
          "hts_code": "6109.10.00.12",
          "description": "Men's (338)",
          "description_path": [
            "T-shirts, singlets, tank tops and similar garments, knitted or crocheted:",
            "Of cotton",
            "Men's or boys':",
            "Other T-shirts:",
            "Men's (338)"
          ],
          "general_rate": "16.5%",
          "special_rates": "Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)",
          "column_2_rate": "90%",
          "rate_source_code": "6109.10.00",
          "source_url": "https://hts.usitc.gov/search?query=6109100012",
          "units": [
            "doz.",
            "kg"
          ],
          "footnotes": [],
          "note": "Rates are those shown in the current HTS schedule; additional duties and eligibility conditions may apply."
        },
        "run_id": "run_01m72hcccpfjaatbegf499rgk8",
        "error": null
      },
      "error": null,
      "method_run_id": "run_01m72hcccpfjaatbegf499rgk8",
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:08.470Z",
      "finished_at": "2026-11-10T10:52:15.291Z"
    },
    {
      "seq": 2,
      "key": "note",
      "path": "note",
      "type": "transform",
      "name": "Write the rates as a note",
      "status": "completed",
      "input": null,
      "output": "HTS 6109.10.00.12 (2026 HTS Revision 22)\nGeneral rate: 16.5%\nSpecial rates: Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)\nColumn 2 rate: 90%\nhttps://hts.usitc.gov/search?query=6109100012",
      "error": null,
      "method_run_id": null,
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:15.293Z",
      "finished_at": "2026-11-10T10:52:15.307Z"
    },
    {
      "seq": 3,
      "key": "send",
      "path": "send",
      "type": "email",
      "name": "Email the note",
      "status": "completed",
      "input": {
        "to": [
          "ada@example.com"
        ],
        "subject": "Import duty for 6109.10.0012",
        "body": "HTS 6109.10.00.12 (2026 HTS Revision 22)\nGeneral rate: 16.5%\nSpecial rates: Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)\nColumn 2 rate: 90%\nhttps://hts.usitc.gov/search?query=6109100012"
      },
      "output": {
        "sent": 1
      },
      "error": null,
      "method_run_id": null,
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:15.310Z",
      "finished_at": "2026-11-10T10:52:15.648Z"
    }
  ]
}
```

### Response 202

```json
{
  "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "status": "running",
  "trigger": "manual",
  "input": {
    "hts_code": "6109.10.0012"
  },
  "error": null,
  "credits": 2,
  "output": null,
  "created_at": "2026-11-10T10:52:08.464Z",
  "started_at": "2026-11-10T10:52:08.465Z",
  "finished_at": null,
  "resume_at": null,
  "steps": [
    {
      "seq": 1,
      "key": "duty",
      "path": "duty",
      "type": "method",
      "name": "Look up the import duty",
      "status": "completed",
      "input": {
        "hts_code": "6109.10.0012"
      },
      "output": {
        "status": "completed",
        "data": {
          "release": "2026 HTS Revision 22",
          "hts_code": "6109.10.00.12",
          "description": "Men's (338)",
          "description_path": [
            "T-shirts, singlets, tank tops and similar garments, knitted or crocheted:",
            "Of cotton",
            "Men's or boys':",
            "Other T-shirts:",
            "Men's (338)"
          ],
          "general_rate": "16.5%",
          "special_rates": "Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)",
          "column_2_rate": "90%",
          "rate_source_code": "6109.10.00",
          "source_url": "https://hts.usitc.gov/search?query=6109100012",
          "units": [
            "doz.",
            "kg"
          ],
          "footnotes": [],
          "note": "Rates are those shown in the current HTS schedule; additional duties and eligibility conditions may apply."
        },
        "run_id": "run_01m72hcccpfjaatbegf499rgk8",
        "error": null
      },
      "error": null,
      "method_run_id": "run_01m72hcccpfjaatbegf499rgk8",
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:08.470Z",
      "finished_at": "2026-11-10T10:52:15.291Z"
    },
    {
      "seq": 2,
      "key": "note",
      "path": "note",
      "type": "transform",
      "name": "Write the rates as a note",
      "status": "completed",
      "input": null,
      "output": "HTS 6109.10.00.12 (2026 HTS Revision 22)\nGeneral rate: 16.5%\nSpecial rates: Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)\nColumn 2 rate: 90%\nhttps://hts.usitc.gov/search?query=6109100012",
      "error": null,
      "method_run_id": null,
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:15.293Z",
      "finished_at": "2026-11-10T10:52:15.307Z"
    },
    {
      "seq": 3,
      "key": "send",
      "path": "send",
      "type": "email",
      "name": "Email the note",
      "status": "running",
      "input": {
        "to": [
          "ada@example.com"
        ],
        "subject": "Import duty for 6109.10.0012",
        "body": "HTS 6109.10.00.12 (2026 HTS Revision 22)\nGeneral rate: 16.5%\nSpecial rates: Free (AU,BH,CL,CO,IL,JO,KR,MA,OM,P,PA,PE,S,SG)\nColumn 2 rate: 90%\nhttps://hts.usitc.gov/search?query=6109100012"
      },
      "output": null,
      "error": null,
      "method_run_id": null,
      "attempts": [],
      "waiting": null,
      "started_at": "2026-11-10T10:52:15.310Z",
      "finished_at": null
    }
  ]
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "wait is a whole number of seconds from 0 to 60.",
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Cancel a workflow run

`POST https://api.fous.com/v1/workflows/{id}/runs/{run_id}/cancel`

Cancels a run that is queued, running or waiting: it does not go on, and a method run its step had going is stopped and free. The steps already finished stay charged (credits on the answer).

### Path parameters

- `id` (string, required)
- `run_id` (string, required)

### Returns

A [WorkflowRun](https://docs.fous.com/api-reference/workflows.md#the-workflowrun-object) object.

- `200`: Canceled.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | No such workflow run |
| 409 | `conflict` | Already ended |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/runs/wfr_01m72hcccgfzqsgbfn5w7q33sv/cancel' \
  -X POST \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq",
  "status": "canceled",
  "trigger": "manual",
  "input": {
    "hts_code": "6109.10.0012"
  },
  "error": {
    "code": "canceled",
    "message": "The run was canceled."
  },
  "credits": 3,
  "output": {
    "sent": 1
  },
  "created_at": "2026-11-10T10:52:08.464Z",
  "started_at": "2026-11-10T10:52:08.465Z",
  "finished_at": "2026-11-10T10:52:15.650Z",
  "resume_at": null,
  "steps": []
}
```

### Error 404

```json
{
  "error": {
    "code": "not_found",
    "message": "No such workflow run.",
    "doc_url": "https://docs.fous.com/guides/errors#not_found",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Renew a webhook address

`POST https://api.fous.com/v1/workflows/{id}/webhook`

Gives a webhook workflow a new address; the old one stops working at once. A webhook workflow gets its first address when it is saved.

### Path parameters

- `id` (string, required)

### Returns

- `200`: The new address.

The answer has these fields:

- `webhook_url` (string, required)

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | No such workflow |
| 409 | `conflict` | Not a webhook workflow |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/webhook' \
  -X POST \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "webhook_url": "https://api.fous.com/v1/hooks/whk_3q9ZxK0cLm2vT8yR5nB1wJ7dF4hG6sPa"
}
```

### Error 404

```json
{
  "error": {
    "code": "not_found",
    "message": "No such workflow.",
    "doc_url": "https://docs.fous.com/guides/errors#not_found",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Renew the signing secret

`POST https://api.fous.com/v1/workflows/{id}/signing-secret`

Gives the workflow a new secret for signing its webhook steps’ calls (X-Fous-Signature); the old one stops signing at once.

### Path parameters

- `id` (string, required)

### Returns

- `200`: The new secret.

The answer has these fields:

- `signing_secret` (string, required)

### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | No such workflow |

### Example request

```bash
curl 'https://api.fous.com/v1/workflows/wf_01m72hccc2f47t2r9qry60m4cq/signing-secret' \
  -X POST \
  -H "Authorization: Bearer $FOUS_API_KEY"
```

### Response 200

```json
{
  "signing_secret": "whsec_3q9ZxK0cLm2vT8yR5nB1wJ7dF4hG6sPa"
}
```

### Error 404

```json
{
  "error": {
    "code": "not_found",
    "message": "No such workflow.",
    "doc_url": "https://docs.fous.com/guides/errors#not_found",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```

## Start a run from a webhook

`POST https://api.fous.com/v1/hooks/{token}`

A webhook workflow’s address (webhook_url): another service POSTs JSON here and its fields are the run’s input, checked against the trigger’s inputs. No API key: the address is the secret. The workflow must be enabled. At most 60 calls a minute per workflow, and 100 of its runs waiting to start. With an Idempotency-Key header (or a sender’s delivery id: webhook-id, svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id), a call that repeats one within 24 hours answers the run the first started (repeated: true) instead of starting another.

No key needed.

### Path parameters

- `token` (string, required)

### Headers

- `Idempotency-Key` (string): The sender’s own id for the call, up to 255 characters. Without it, a sender’s delivery id is used the same way: webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery or X-Shopify-Webhook-Id.

### Request body

Optional, JSON.

### Returns

- `202`: Queued.

The answer has these fields:

- `run_id` (string, required)
- `workflow_id` (string, required)
- `repeated` (boolean): The Idempotency-Key was sent within 24 hours: its run.

### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The body does not fit the trigger’s inputs |
| 403 | `plan_limit` | Schedules and webhooks come with a paid plan (limit.kind api) |
| 404 | `not_found` | No workflow at this address |
| 409 | `conflict` | The workflow is turned off |
| 429 | `rate_limited` | Too many calls this minute, or 100 runs waiting to start |

### Example request

```bash
curl 'https://api.fous.com/v1/hooks/whk_3q9ZxK0cLm2vT8yR5nB1wJ7dF4hG6sPa' \
  -H "Idempotency-Key: $FOUS_IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "order_id": 1042,
    "customer": {
      "name": "Ada"
    }
  }'
```

### Response 202

```json
{
  "run_id": "wfr_01m72hcccgfzqsgbfn5w7q33sv",
  "workflow_id": "wf_01m72hccc2f47t2r9qry60m4cq"
}
```

### Error 400

```json
{
  "error": {
    "code": "invalid_request",
    "message": "input.order_id: Required.",
    "errors": [
      {
        "path": "input.order_id",
        "message": "Required."
      }
    ],
    "doc_url": "https://docs.fous.com/guides/errors#invalid_request",
    "request_id": "req_mh7c2k9xQ1v3Zt8LbN0pRwY4"
  }
}
```
