# How a recipe is composed

{/* Generated from backend/features/recipes/schemas.py and the recipe catalogue by `make arch`. Never edit this page: a new kind, trigger or bound changes it, and `--check` fails until it is regenerated. */}

A recipe is a feature written as configuration: a named, versioned bundle of the platform's own primitives -- collections, tools, flows, automations, a tool placed in an agent's slot and a flow placed as a connection's thread flow -- and the parameters it asks the organization for. Each page in this cookbook is made from one recipe, and this page explains every word they use, read from the code that validates them. Read [How it fits together](/how-it-fits) first for the model they are made of.

## The recipes

- [Mail triage](/cookbook/mail-triage): Decide what each mail on one connection calls for -- answer, draft, internal only, escalate or ignore -- with a small model, and wake your agent only for the mail that needs it.
- [Memory](/cookbook/memory): Give one agent a memory: notes about each person and about the business, taken after every conversation and handed back at the start of the next one.
- [Triage](/cookbook/triage): Sort each new thread of one connection under the organization's labels, sending some to people and the rest to an agent.
- [Weekly summary](/cookbook/weekly-summary): Tell one person, once a week, what one of the organization's collections gained, in a few short paragraphs a model writes.

## A recipe

A recipe declares `key`, `version`, `name`, `description`, `intent`, `adapt`, `headline` (optional), `setup`, `setup_blank` (optional), `mark` (optional), `es` (optional), `requires` (optional), `parameters` (optional), `resources`, `proposed_labels` (optional). Its `intent` is one sentence on one line and its `adapt` a note on how to make it your own; neither, nor any part's `says`, may hold a `${...}` placeholder, because they are read as written.

A recipe also carries its `setup`: one sentence, at most 200 characters, in the client's words, that says what the recipe will do with what it is given. Each parameter it asks is a hole in the sentence, written `{name}` after the parameter; every parameter without a default is a hole, once, and one with a default may be a hole or may be left out. A recipe's page publishes the sentence with each hole shown as what its parameter is called. Every language a recipe speaks holds the same holes, in the order its own grammar wants (rule 45).

A recipe may propose labels for the organization's vocabulary, each a name, the line saying what belongs under it and whether it joins the one parameter that chooses labels they are for. The install creates the ones the client keeps as the organization's own labels: never the recipe's, never locked, and left in place by an update and by uninstalling.

At most 20 parameters and 50 parts.

## Parts and their keys

Every key -- a parameter's name, a part's, a flow step's -- is spelled `^[a-z][a-z0-9_]*$` and is stable: parts point at one another by key, never by id, so the same recipe installs the same way in every organization. A part names only parts declared before it. The kinds of part:

- **collection**: `key`, `name`, `says`, `description` (optional), `fields`, `identity` (optional), `contact` (optional), `editable` (optional), `records` (optional).
- **tool**: `key`, `name`, `says`, `description` (optional), `builtin` (optional), `flow` (optional), `config` (optional), `fixed` (optional), `input_schema` (optional), `deadline_seconds` (optional), `when` (optional).
- **flow**: `key`, `name`, `says`, `steps`, `edges` (optional), `entry` (optional).
- **automation**: `key`, `name`, `says`, `flow`, `cron` (optional), `trigger` (optional), `fan_out` (optional), `enabled` (optional).
- **placement**: `key`, `name`, `says`, `agent`, `slot`, `tool`.
- **thread_flow**: `key`, `name`, `says`, `connection`, `flow`.

A flow's step takes `key`, `tool`, `params` (optional), `when` (optional), `unless` (optional); an edge `from`, `to`, `condition` (optional), `when` (optional), `unless` (optional). A step's edges are tried in the order the flow declares them, and the first whose condition holds is taken.

A tool, a step and an edge may be made only while a parameter has a value: `when: ${param.<name>}` leaves it out when that value is blank -- none, an empty text, an empty list, a no -- and, on a step or an edge, `unless: ${param.<name>}` leaves it out when it is not. An edge to or from a step left out goes with it, and the entry step is never left out. A parameter that may be left blank is read only by what such a `when` makes, and so is a tool made only `when` one has a value; a key of a tool's `config` that is one whole `${param.<name>}` token is left out of the tool while that value is blank.

A collection may mark one personal field as the `contact` its records are about, offer its records for editing (`editable`), and say what retention does to its records past the window (`records`): `personal_fields`, `whole` -- `whole` deletes a record with a personal field whole, and the recipe's page says so.

Each kind is made by hand with these public operations; each recipe's page names them part by part:

- collection: `POST /api/v1/collections` creates the collection with its fields; `PATCH /api/v1/collections/{collection_id}` sets the identity field once the fields exist.
- tool: `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.
- flow: `POST /api/v1/flows` creates the flow; `POST /api/v1/flows/{flow_id}/steps` adds each step, naming the tool it runs; `POST /api/v1/flows/{flow_id}/edges` adds each edge; `PATCH /api/v1/flows/{flow_id}/entry-step` sets the step a run starts at.
- automation: `POST /api/v1/automations` creates the automation on its trigger, running the flow; `PUT /api/v1/automations/{automation_id}/enabled` switches it on: an automation is created switched off.
- placement: no public operation of its own yet; the nearest, tied to a version: `PATCH /api/v1/agents/versions/{version_id}` adds the tool to the draft's `pre_hook_tool_ids` or `post_hook_tool_ids`; `POST /api/v1/agents/versions/{version_id}/publish` publishes the draft.
- thread_flow: no public operation of its own yet; the nearest, tied to a version: `POST /api/v1/channels` makes the connection's channel, answered by the flow it names.

## From a recipe to the API

A recipe's YAML is its declaration, not a request body. Building a part by hand, send each field as below: a value written `${...}` is replaced with your own, and a part, step or agent named by key is named by the id its own operation answered.

### collection

| Recipe field | Operation | Request field | Note |
|---|---|---|---|
| `key` | never sent | — | the recipe's own |
| `name` | `POST /api/v1/collections` | `name` |  |
| `says` | never sent | — | the recipe's own |
| `description` | `POST /api/v1/collections` | `description` |  |
| `fields` | `POST /api/v1/collections` | `fields` | the same shape, one entry per field |
| `identity` | `PATCH /api/v1/collections/{collection_id}` | `identity_field_key` | once the fields exist |
| `contact` | never sent | — | no request field yet: a mark the recipe keeps |
| `editable` | never sent | — | what the recipe's own page offers |
| `records` | never sent | — | no request field yet: a mark the recipe keeps for retention |

### tool

| Recipe field | Operation | Request field | Note |
|---|---|---|---|
| `key` | never sent | — | the recipe's own |
| `name` | `POST /api/v1/tools` | `name` |  |
| `says` | never sent | — | the recipe's own |
| `description` | `POST /api/v1/tools` | `description` |  |
| `builtin` | `POST /api/v1/tools` | `source`, `config.builtin` | `source` is `"builtin"`, and the built-in's name goes in `config.builtin` |
| `flow` | `POST /api/v1/tools` | `source`, `config.flow_id` | `source` is `"builtin"`, and the flow's id goes in `config.flow_id` |
| `config` | `POST /api/v1/tools` | `config` | beside the key above |
| `fixed` | `POST /api/v1/tools` | `fixed_parameters` |  |
| `input_schema` | `POST /api/v1/tools` | `input_schema` |  |
| `deadline_seconds` | `POST /api/v1/tools` | `deadline_seconds` |  |
| `when` | never sent | — | never sent: made only while the parameter it names has a value |

### flow

| Recipe field | Operation | Request field | Note |
|---|---|---|---|
| `key` | never sent | — | the recipe's own |
| `name` | `POST /api/v1/flows` | `name` |  |
| `says` | never sent | — | the recipe's own |
| `steps` | `POST /api/v1/flows/{flow_id}/steps` | — | one call per step, in order; the first added is the entry |
| `steps[].key` | never sent | — | never sent: the call answers the step's id, which every later `$ref:` and edge names instead |
| `steps[].tool` | `POST /api/v1/flows/{flow_id}/steps` | `tool_id` | the tool's id |
| `steps[].params` | `POST /api/v1/flows/{flow_id}/steps` | `param_overrides` | each `$ref:<step key>` written `$ref:<that step's id>` |
| `edges` | `POST /api/v1/flows/{flow_id}/edges` | — | one call per edge |
| `edges[].from` | `POST /api/v1/flows/{flow_id}/edges` | `source_step_id` | the step's id |
| `edges[].to` | `POST /api/v1/flows/{flow_id}/edges` | `target_step_id` | the step's id |
| `edges[].condition` | `POST /api/v1/flows/{flow_id}/edges` | `condition` | `field`, `op`, `value`; `field` reads `<step id>.<path>` |
| `steps[].when` | never sent | — | never sent: made only while the parameter it names has a value |
| `steps[].unless` | never sent | — | never sent: made only while the parameter it names is left blank |
| `edges[].when` | never sent | — | never sent: made only while the parameter it names has a value |
| `edges[].unless` | never sent | — | never sent: made only while the parameter it names is left blank |
| `entry` | `PATCH /api/v1/flows/{flow_id}/entry-step` | `step_id` | only when it is not the first step |

### automation

| Recipe field | Operation | Request field | Note |
|---|---|---|---|
| `key` | never sent | — | the recipe's own |
| `name` | `POST /api/v1/automations` | `name` |  |
| `says` | never sent | — | the recipe's own |
| `flow` | `POST /api/v1/automations` | `flow_id` | the flow's id |
| `cron` | `POST /api/v1/automations` | `trigger_kind`, `trigger_config.cron` | `trigger_kind` is `"schedule"` |
| `trigger` | `POST /api/v1/automations` | `trigger_kind` | its one key |
| `trigger.schedule.cron` | `POST /api/v1/automations` | `trigger_config.cron` |  |
| `trigger.interaction_ended.agent` | `POST /api/v1/automations` | `trigger_config.agent_id` | the agent's id |
| `trigger.interaction_ended.end_reasons` | `POST /api/v1/automations` | `trigger_config.end_reasons` |  |
| `trigger.interaction_ended.leave_out_tests` | `POST /api/v1/automations` | `trigger_config.leave_out_tests` |  |
| `trigger.record_written.collection` | `POST /api/v1/automations` | `trigger_config.collection_id` | the collection's id |
| `fan_out` | `POST /api/v1/automations` | `fan_out` |  |
| `fan_out.collection` | `POST /api/v1/automations` | `fan_out.collection_id` | the collection's id |
| `fan_out.filters` | `POST /api/v1/automations` | `fan_out.filters` |  |
| `fan_out.limit` | `POST /api/v1/automations` | `fan_out.limit` |  |
| `enabled` | `PUT /api/v1/automations/{automation_id}/enabled` | `enabled` |  |

### placement

| Recipe field | Operation | Request field | Note |
|---|---|---|---|
| `key` | never sent | — | the recipe's own |
| `name` | never sent | — | the recipe's own |
| `says` | never sent | — | the recipe's own |
| `agent` | `PATCH /api/v1/agents/versions/{version_id}` | — | the agent whose draft version is `version_id` |
| `slot` | `PATCH /api/v1/agents/versions/{version_id}` | `pre_hook_tool_ids`, `post_hook_tool_ids` | `pre` is the first list, `post` the second |
| `tool` | `PATCH /api/v1/agents/versions/{version_id}` | — | the tool's id, added to that list |

### thread_flow

| Recipe field | Operation | Request field | Note |
|---|---|---|---|
| `key` | never sent | — | the recipe's own |
| `name` | never sent | — | the recipe's own |
| `says` | never sent | — | the recipe's own |
| `connection` | `POST /api/v1/channels` | `connection_id` | the connection the channel is made on; the parameter names its channel |
| `flow` | `POST /api/v1/channels` | `flow_id` | the flow's id |


## References

A string anywhere in a part may hold `${<head>.<name>[.<attribute>]}`:

- `${param.<name>}` -- a parameter's value. Attributes: a collection parameter's `get_tool`, `name`, `query_tool`, `write_tool`; a weekly_moment parameter's `cron`, `hour`, `weekday`; an agent parameter's `name`; a connection parameter's `name`.
- `${resource.<key>}` -- the id of a part declared earlier. Attributes: a collection's `get_tool`, `name`, `query_tool`, `write_tool`.
- `${platform.<name>}` -- a value of the deployment's: `mail_sender`.

A placeholder that is the whole string keeps the value's type; one inside a longer string is written into it. A field that names one thing -- an automation's flow, a placement's agent and tool -- is one whole placeholder of the right kind.

A flow step's parameters also speak the flow's own grammar: `$ref:<step>.<path>` is what an earlier step answered, and `$ref:trigger.<path>` is what set the run off.

A tool's `fixed` values may name a fact of the conversation the tool runs in, and only one every slot keeps, after the conversation included: `$ref:conversation.contact`, `$ref:conversation.id`, `$ref:conversation.number`. It is read only there: a flow placed in a slot reads it as `$ref:trigger.<key>`, handed in by the tool that runs it.

## Parameters

A parameter takes `name`, `kind`, `label`, `description` (optional), `required` (optional), `default` (optional), `minimum` (optional), `maximum` (optional), and is either required or has a default. Its kind is one of:

- `text`
- `number`
- `boolean`
- `date`
- `email`
- `collection`
- `weekly_moment`
- `agent`
- `connection`
- `labels`
- `person`

Only a `number` takes a range, `minimum` and `maximum`, either end optional; a value outside it is refused, saying the range. A recipe asks for one `agent` at most, always required and fixed once installed: it says which install this is, one per agent. It asks for one `connection` at most -- a connection that takes threads, by its channel's id -- always required and fixed once installed, one install per connection; beside it, the agent does not say which install this is, so it may be left blank, and is fixed once installed too. A `labels` or `person` parameter may be left blank in any recipe, and then is none. A recipe never asks for a secret: a credential lives in a connection a parameter points at.

## A connection's thread flow

A `thread_flow` part places one of the recipe's flows as the thread flow of the connection its connection parameter names, in place of the one the connection answered with: a flow the connection wrote goes, and one built by hand stays yours. The connection runs it on every message, behind its own who reaches agents and reply mode, and what set the run off is the burst, read as `$ref:trigger.<path>`. While the recipe is off, a message that would run the flow waits for a person, saying why; uninstalling leaves the connection answered by the agent the recipe named, or by a person.

## Slots

A placement puts one of the recipe's tools -- a built-in's instance, or a flow offered as a tool -- in an agent's slot, outside its versions: it runs from the agent's next conversation without a publish and stays through every publish. The slots:

- `pre`
- `post`

## Triggers

An automation runs one of the recipe's flows. A bare `cron` is the schedule written short; otherwise its `trigger` is exactly one of:

- `schedule`: takes `cron`
- `interaction_ended`: takes `agent`, `end_reasons`, `leave_out_tests`
- `record_written`: takes `collection`
- `webhook`: takes nothing

With `fan_out` (`collection`, `filters` (optional), `limit` (optional)) it opens one run per matching record, at most 200. An automation also takes `enabled` (optional).

## Rows written by one step

A collection's write tool takes `rows`, a list of rows written as one record each, in order, at most 200 from one step; a longer list fails the step before any row is written. Its query tool takes `newest_first` to read the newest records first, and a `limit`.

The drafting built-in `draft_text` returns rows instead of a text when its instance's `config` names a row's fields under `rows`: its answer is then `rows`, ready for a write step.

## The step that replaces sensitive numbers

The built-in `redact_sensitive_numbers` takes a text, or any value whose strings it walks, and returns it with card numbers, bank accounts and identity numbers replaced, keeping nothing of what it replaced. Put it between a drafting step and the write that keeps the result.

## What it costs

Nothing is charged for a recipe itself: what its parts consume -- a tool execution, a model's tokens, a message, storage -- is charged as those parts are.
