# Create a workflow

> Draft a workflow from a sentence, stream it as it is written, write one as JSON, build missing methods, and change it.

Source: https://docs.fous.com/guides/workflows/create
Index: https://docs.fous.com/llms.txt

## From a sentence

Send `prompt`. Fous reads your methods, picks the ones the request needs, wires their inputs and writes the steps between them.

```bash
curl https://api.fous.com/v1/workflows \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Every Monday at 9, list open invoices on our portal and email me a digest",
    "timezone": "Europe/Berlin"
  }'
```

Returns `200` with the saved workflow, disabled, plus an `ai` object describing what the AI built (a `definition` sent instead returns `201`). `timezone` (optional) sets the time zone for times like "9am"; without it, schedules use UTC.

```json
{
  "id": "wf_01m6vnc9ewe3za9g5kqvd8yrkp",
  "name": "Weekly open invoices digest",
  "trigger_type": "schedule",
  "enabled": false,
  "step_count": 3,
  "methods": ["portal.example.com/list_invoices"],
  "incomplete": [],
  "ai": {
    "summary": "Every Monday at 9:00 Berlin time it lists your open invoices on portal.example.com, writes a digest and emails it to you at ada@example.com.",
    "missing": [],
    "reply": null,
    "question": null
  }
}
```

The response also includes `definition`. The AI uses only your organization's methods and the inputs their contracts accept. An email step goes to the person making the request unless the prompt names other addresses; with an API key there is no person, so `to` is left empty.

A value the AI cannot know, such as a webhook URL you did not provide, is left empty and listed in `incomplete`. When a choice has a sensible default, the AI uses it and says so in `summary`.

```json
{
  "ai": {
    "summary": "Every Monday at 9:00 it lists your open invoices and posts them to your webhook.",
    "missing": [],
    "reply": null,
    "question": null
  },
  "incomplete": [{ "key": "post", "field": "url", "message": "Add the webhook address" }]
}
```

Until each value is set with `PATCH /v1/workflows/{id}`, the workflow cannot run or be enabled: both return `409 conflict` with `reason: "workflow_incomplete"`, the `incomplete` values and any pending `steps`.

### When the AI asks or replies instead

If a step needs a website or account the AI cannot determine ("our supplier portal", "our bank"), the AI asks instead of building. Nothing is saved, and the response is `200` with `workflow: null`:

```json
{
  "workflow": null,
  "ai": {
    "summary": null,
    "missing": [],
    "reply": "I can build this. Which supplier portal do you use?",
    "question": {
      "text": "Which supplier portal do you use?",
      "options": [{ "label": "Example Portal", "value": "portal.example.com" }],
      "placeholder": "Portal address, e.g. portal.example.com",
      "allow_other": true
    }
  }
}
```

To answer, send the conversation as `messages`, newest last, ending with the person's answer:

```json
{
  "messages": [
    { "role": "user", "text": "Each day, summarize new invoices and email finance" },
    { "role": "assistant", "text": "I can build this. Which supplier portal do you use?" },
    { "role": "user", "text": "portal.example.com" }
  ]
}
```

A prompt that requests no automation ("what can you do?") also saves nothing and returns `200` with `workflow: null` and the answer in `ai.reply`. If the AI is unavailable, the response is `503 service_unavailable` and nothing is saved.

### Watch it being written

With `Accept: text/event-stream`, the same request returns Server-Sent Events while the AI writes, so you can render the workflow step by step.

```bash
curl -N https://api.fous.com/v1/workflows \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{ "prompt": "Track shipment EX123456789US on carrier.example.com and post its status to https://hooks.example.com/shipments" }'
```

Events are sent as they happen:

```text
event: status
data: {"text":"Reading your 6 methods"}

event: workflow
data: {"name":"Shipment status to webhook","description":"Tracks a shipment and posts its status."}

event: trigger
data: {"trigger":{"type":"manual","inputs":[]}}

event: step
data: {"index":0,"step":{"key":"track","name":"Track the shipment","type":"method","method":"carrier.example.com/track_shipment","input":{"tracking_number":"EX123456789US"}}}

event: done
data: {"id":"wf_01m6vnd2j8fnh9m45hvyj54nza","name":"Shipment status to webhook","incomplete":[],"ai":{"summary":"When you press Run, it tracks shipment EX123456789US and posts its status to your webhook.","missing":[],"reply":null,"question":null}}
```

| Event | Data |
| --- | --- |
| `status` | `{ text }`: a short description of what the AI is doing. |
| `question` | `{ text, options, placeholder, allow_other }`: the reply asks the person to choose. |
| `reply` | `{ text }`: the next piece of a reply, when the AI replies instead of building. |
| `workflow` | `{ name, description }`, as soon as they are written. |
| `trigger` | `{ trigger }`. |
| `step` | `{ index, step }`. Emitted as each top-level step is completed, in order. Loops are emitted with all nested steps included. |
| `reset` | `{}`. The initial draft failed validation and the AI is restarting; discard earlier streamed events. |
| `done` | The result, the same body as the JSON response: the workflow with `ai`, or `{ workflow: null, ai }`. |
| `error` | The [error](https://docs.fous.com/guides/errors.md) object, as in a JSON response. |

A step in a `step` event may not have its `key` yet; the definition in `done` has them all.

## From a definition

`POST /v1/workflows` also accepts `{ name, description, definition, enabled }`, as in [the example workflow](https://docs.fous.com/guides/workflows.md#create-it). A definition is a `trigger` and a list of [`steps`](https://docs.fous.com/guides/workflows/steps.md). A body with `prompt` or `messages` is drafted by the AI; any other body is saved as sent.

Fous validates the definition against your methods. An invalid definition returns `400 invalid_request` listing all validation errors and their paths:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "2 problems, first steps[0].input.colour: Not an input of portal.example.com/list_invoices. It takes: status, due_before.",
    "errors": [
      {
        "path": "steps[0].input.colour",
        "message": "Not an input of portal.example.com/list_invoices. It takes: status, due_before."
      },
      {
        "path": "steps[1].code",
        "message": "The code reads step \"send\", which has not run yet here: only steps before this one can be read."
      }
    ]
  }
}
```

A definition is at most 200 KB. A method step must name one of your methods by its slug and use only that method's inputs. Templates and transform code can read only the trigger, earlier steps and, inside a loop, the current item.

## Build missing methods

If a request needs a site your organization has no method for, the AI still writes the whole workflow. In place of the missing method it adds a pending step: `method` is `null`, and `pending` gives what the method should do (`need`), the `site` to build on, and the request sent to the builder (`build_prompt`). Later steps read its output using field names inferred from the need, and the step is listed in `ai.missing`.

```json
{
  "key": "rates",
  "name": "Get today's rates",
  "type": "method",
  "method": null,
  "input": {},
  "pending": {
    "need": "Get the latest exchange rates for the euro against chosen currencies.",
    "site": "rates.example.com",
    "build_prompt": "On rates.example.com, get the latest exchange rates for a base currency against a list of target currencies.",
    "build_id": null,
    "status": "proposed"
  }
}
```

While a step is pending, the workflow is saved but cannot run or be enabled: both return `409 conflict` with `reason: "workflow_incomplete"` and the pending `steps`. To build the method and set its site in one request:

```bash
curl -X POST https://api.fous.com/v1/workflows/wf_01m6vqpc6yebka97gthqhbype8/steps/rates/build \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "site": "rates.example.com" }'
```

Returns `202` with `build_id`. This is an ordinary [build](https://docs.fous.com/guides/builds.md), subject to your plan's limits. The body is optional:

| Field | Meaning |
| --- | --- |
| `site` | The website to build on, as a domain or sign-in URL. Replaces the step's site. Saved only when the build starts, so a refused build changes nothing. |
| `connections` | IDs of the connected accounts the build may use. |

`pending.status` changes to `building`. Poll `GET /v1/workflows/{id}` every few seconds. When the build ends, the step runs the new method: Fous fills its input from the method's contract and updates later steps to read its real output.

- A failed build sets `pending.status` to `failed` with a `reason`. Call the endpoint again to retry.
- A step with no site and no `site` in the request returns `409` with `reason: "site_required"`.
- A step that is already building returns its `build_id`. Sending a different `site` for it returns `409 conflict`.
- To remove a step, pending or not, call `DELETE /v1/workflows/{id}/steps/{key}`. What read its output reads nothing: templates are emptied, a filter drops conditions about it, and a transform whose code read it gets new code (a transform with code and no instruction is not rewritten: the removal answers `400` naming it, so change its code first). A value left empty is listed in `incomplete`.

## Change a workflow

`PATCH /v1/workflows/{id}` updates any of `name`, `description`, `definition` and `enabled`. A new definition is validated as above and replaces the old one entirely. Enabling a workflow that is not ready returns `409` with `reason: "workflow_incomplete"`. `DELETE /v1/workflows/{id}` deletes a workflow; its runs stay readable. `GET /v1/workflows` lists workflows, most recently changed first.

### Change it with the AI

Send `PATCH /v1/workflows/{id}` with `prompt`, or the conversation so far as `messages`, newest last, and the AI changes the workflow and saves it:

```bash
curl -X PATCH https://api.fous.com/v1/workflows/wf_01m6vnc9ewe3za9g5kqvd8yrkp \
  -H "Authorization: Bearer $FOUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "make it run every day at 8",
    "timezone": "Europe/Berlin"
  }'
```

The response is the saved workflow with `ai`, as when [creating from a sentence](#from-a-sentence):

```json
{
  "id": "wf_01m6vnc9ewe3za9g5kqvd8yrkp",
  "name": "Weekly open invoices digest",
  "enabled": false,
  "ai": {
    "summary": "It now runs every day at 8:00 Berlin time.",
    "missing": [],
    "reply": null,
    "question": null
  }
}
```

The AI keeps everything you did not ask to change and reads the last 12 messages. A question, or the AI asking one back, changes nothing: the answer is the workflow as it is, with `ai.summary` `null` and its words in `ai.reply` (or `ai.question`). With `Accept: text/event-stream` the response streams as [above](#watch-it-being-written), and `done` carries the same body.

A change is saved with the same checks as any `PATCH`, and a refused change saves nothing: `400 invalid_request`, `403 plan_limit`, or `409` with `reason: "workflow_incomplete"` when the workflow is enabled and the change leaves it not ready. If the AI's answer does not hold after a retry, the response is `422 invalid_input` with `reason: "ai_failed"`.

Add `"preview": true` to see the change without saving it: the response is the workflow as it would be, and `name` and `definition` in the request are the draft the AI reads. Send it back with `PATCH` to keep it.

The AI costs no credits within your plan's [AI messages](https://docs.fous.com/guides/billing.md#builds-and-ai).
