# How it fits together

Read this page before the API reference. The reference lists two hundred
operations; this page says which few you need for a given job, and in which
order. Every rule here is enforced by the API, so a design that breaks one
is refused, not warned about.

## The model

This platform builds conversational agents from a small set of pieces, and almost every request is met by composing those pieces, not by writing code.

An agent is a graph of nodes. Each node holds instructions and the tools the model may call while it is there. Everything the model can do is a tool, and the model sees every tool the same way, whatever it is underneath.

A tool does one thing: query a collection, write a record, send an email, call one endpoint of the client's own system. A tool never carries a credential. Authorization is a connection, made once per provider and pointed at by any number of tools. A tool never carries a business fact. Opening hours, prices and policies are records in a collection (custom data), read by the tool that needs them.

When a job needs more than one tool, or a branch, it is a flow: a deterministic graph of steps, where each step is a tool and each edge is a condition on what earlier steps returned. A flow is itself a tool, so an agent calls it by one name. When the job must happen with no conversation at all, on a schedule, when a record changes, when an interaction ends, or when the client's system calls us, it is an automation: a trigger plus a flow.

A custom tool is code, written for one account, that reshapes data: it is handed arguments and answers a value. It cannot reach the network or any system. It exists for the one calculation or format nothing else provides, and its input is what other tools answered, so it sits in a flow as one step among the others: the flow hands it the price that was looked up or the list that was fetched, exactly. An agent that reads a value with one tool and types it into the next is the shape a flow replaces.

So, faced with a requirement: put what the agent should say in its node instructions; put the facts in custom data; put the credentials in a connection; give each action its own tool; combine and branch with a flow; schedule with an automation; write a custom tool only for a transform no piece provides. Never write one tool that does the whole job.

## The pieces

| Piece | What it is | What it is not |
|---|---|---|
| **Agent** | A graph of nodes, each with instructions and tools. Reached through a channel: voice, text, messaging, API. Edited as a draft, published as a version. | Not a "voice agent" or a "text agent". One agent, many channels. |
| **Node** | One situation in a conversation: its instructions and the tools available there. The model moves between nodes by deciding, not by conditions. | Not a step in a procedure. Procedures are flows. |
| **Tool** | One action with a name and a schema. Built-in, imported from an MCP server, a flow, or custom code: the model cannot tell. Configured as a named instance, attached globally, to a node, or as a pre/post hook. | Not a place for credentials or business facts. |
| **Connection** | One authorization to one outside provider: a pasted key, a messaging account, or the client's own system's key (provider `http_key`). Pointed at by tools. | Not a tool. It does nothing by itself. |
| **Custom data** | Collections of records with a declared schema, owned by the organization. Each collection comes with its own `query_`, `get_` and `write_` tools the moment it is created. | Not a document library. Documents the agent quotes from are RAG. |
| **Flow** | A directed graph of steps. Each step runs a tool; edges branch on conditions over earlier results; later steps read earlier outputs. Exactly one entry step; every run ends. | Not conversational. The model never decides inside a flow. |
| **Automation** | A trigger plus a flow. A schedule, a record written, an interaction ended, or an outside system calling a webhook; or run by hand. Created switched off. | Not a second workflow engine. It only decides *when*. |
| **Custom tool** | Starlark code for one account: one `transform(arguments)` function that returns a value. Draft, publish, rollback, like an agent. | Not an integration. Its declared reach must be empty. |
| **Channel** | Where messages from contacts arrive (WhatsApp first). Every message runs the channel's own flow, which may hand it to an agent. | Not a trigger for automations. |

## The ladder

When a requirement arrives, walk this list from the top and stop at the
first rung that meets it. Each rung is cheaper to build, cheaper to change,
and more visible to the client than the one below it.

1. **Node instructions.** What the agent should say or how it should behave
   with no lookup at all. "Be brief", "always confirm the date", "hand off to a
   person if asked" belong here, together with the built-in tools for call
   control (transfer, hang up).
2. **Custom data.** Any fact of the business: hours, prices, locations, the
   list of services, the people on shift. Create a collection with a schema;
   its query, get and write tools appear with it, and the agent reads what it
   needs at run time. A fact written inside a tool or a prompt is a fact that
   goes stale the day it changes.
3. **Connection.** Any credential: a provider key, a messaging account, the
   API key of the client's own system. Create the connection once and point
   tools at it. A tool whose configuration carries a key or an auth header is
   refused when it is created.
4. **One tool per action.** Look up availability. Create the booking. Send the
   email. Call this one endpoint. Each is a named instance, reusable across
   agents. The HTTP tool, pointed at a connection, is how the client's own
   system is reached. A single tool that reads, decides and writes is the shape
   to avoid: it cannot be tested, reused, or rolled back one part at a time.
5. **Flow.** As soon as the job is "do this, then that, unless…". Steps are
   the tools from rung 4; edges branch on their results; the flow's result is
   the last step's output. The flow is then registered as a tool with one name,
   so the agent calls it as a whole and the model never sees the steps. Two tool
   calls the model would make one after the other, handing the first's answer to
   the second, are a flow too: the flow passes the value exactly, with `$ref`,
   where a model copies it. A prompt is not a place to describe a procedure; a
   flow is.
6. **Automation.** As soon as the job has a *when* and no conversation:
   every morning, when a record is written, when an interaction ends, when the
   client's system calls us. The automation supplies the trigger; the flow from
   rung 5 supplies the work.
7. **Custom tool.** Only for a transform no rung above provides: a discount
   formula, a file format nobody else has, a slot calculation over two lists.
   It is handed arguments and answers a value; it reaches nothing. It drops
   into a flow as one step, between the tools that fetched its input and the
   tool that acts on its output. A quote from a price list with a discount
   formula is a flow of two steps, query then formula, registered as one tool;
   not a custom tool on the node.

If a requirement seems to need something below rung 7, the platform cannot
do it today. Say so; do not build around it.

## One versus the other

| When you are choosing between | Reach for | Because |
|---|---|---|
| A **flow** and the **node graph** | The node graph when the *model* should decide from what the person says; a flow when *data* should decide. | Conversation-level branching is LLM-driven. Inside-a-tool branching is deterministic. They never compete. |
| A **flow** and a **custom tool** | A flow whenever the steps are existing tools. | Custom code fills a gap in the toolbox; it does not replace the toolbox. |
| A **custom tool** on a node and **in a flow** | In a flow, after the tools that fetch its input. | The model never carries one tool's answer into another by hand. A flow passes it exactly with `$ref`, and the model sees one tool with one name. |
| An **HTTP tool** and a **custom tool** | The HTTP tool for anything that calls a system. | A custom tool cannot make a request. The HTTP tool, pointed at a connection, is how the client's own system is reached. |
| A **connection** and a tool **setting** | The connection for anything secret. | A tool never carries a credential. The rule is enforced on create and update. |
| **Custom data** and **RAG** | Custom data for facts the agent reads and writes as fields; RAG for documents it quotes from. | A price list is fields. A brochure is a document. |
| **Custom data** and the **prompt** | Custom data for anything that will change. | The prompt is republished; a record is edited. |
| An **automation** and a flow **called by the agent** | The automation when there is no conversation. | The same flow runs either way. Only who starts it differs. |
| An **automation** and a **channel flow** | The channel flow for anything that reacts to an incoming message. | A message on a channel already runs a flow. It is not an automation trigger. |
| A **pre hook** and a **flow step** | A pre hook for what the agent should know before the first word: the caller's record, their last order. | Pre hooks run once per conversation and inject context; flows run when called. |
| A **post hook** and an **automation on interaction ended** | Either; the automation when the work has branches or several steps. | Post hooks take one tool; automations take a flow. |

## What never goes inside a tool

- **A business fact.** Opening hours hardcoded in a tool are wrong the first
  holiday. They are records in a collection.
- **A credential.** An API key written on a tool is readable by anyone who
  can read the tool. It is a connection.
- **A second action.** A tool that reads and then writes cannot be tested
  or reused apart. It is two tools and a flow.
- **A decision.** "If there is a slot, book it, else say no" is an edge in a
  flow, not a branch in code.
- **A schedule.** "Every Monday" is an automation trigger, not a loop.

## A worked example

*"The agent must book appointments and must never double-book."* The
client's bookings live in their own system, behind an API key.

The shape to avoid is one tool that authenticates, filters out the hours the
business is closed, checks availability, and books. Nothing in it can be
changed, tested or reused apart from the rest, and the hours are wrong the
first holiday.

The composition:

1. A **connection** with provider `http_key` holding the booking system's key
   and the header it travels in.
2. A **collection** `opening_hours` with the business's hours as records.
3. Two **HTTP tools**, both pointing at the connection: one fetches the
   day's bookings, one creates a booking.
4. A **custom tool** `free_slots` that takes the hours and the day's bookings
   and answers the free slots. Pure calculation, no I/O.
5. A **flow** `book_appointment`: query hours, fetch bookings, compute free
   slots, then an edge on the result. If the requested time is free, create the
   booking; otherwise the flow ends with the alternatives, and the agent says
   the time is taken.
6. The flow **registered as a tool** and attached to the node where booking
   happens. The model sees one tool, `book_appointment`, with a date and a time.

The exact sequence of operations is the recipe
[Book an appointment without double-booking](/recipes/book-appointment).

## One key per person

Where the client's system gives each person their own key, make one
connection per person, each with the same `group` and the person's
`subject_number` (an international number) or `subject_ref` (their id in the
client's system). Point the tool at the group with `connection_group` instead of
at one `connection_id`. Each time the tool runs, the platform picks the
connection of whoever the conversation is with. No match fails the tool before
anything is sent; there is never a fallback to another person's key.

On a **call**, the person is identified by the phone number alone: the caller ID
on an inbound call, the number dialled on an outbound one. A `subject_ref` never
identifies anyone on a call, whatever the call's context says.

> **The caller ID on an inbound call is trusted as who is calling, and it can be
> spoofed.** Anybody who presents a salesperson's number when ringing your line
> runs a group tool with that salesperson's key. Put a group tool on an inbound
> agent only where that is acceptable.

### A grant per roster entry

The other shape is one connection with `per_person: true`, holding one grant for
each person on the organization's roster. A tool pointing at it runs with the
grant of whoever the conversation is with. Do both steps from your server with
an organization API key made by an owner or an admin; a member's key reaches only
its creator's own entry and adds no one.

First find the employee's entry by their phone. `list_roster` returns every entry
with its addresses, phones in international form; look for the number there:

```bash
curl https://YOUR_PLATFORM_ORIGIN/api/v1/roster \
  -H "Authorization: Bearer $ATTEQ_API_KEY"
```

If nobody has it, add them. The answer's `id` is the entry id:

```bash
curl -X POST https://YOUR_PLATFORM_ORIGIN/api/v1/roster \
  -H "Authorization: Bearer $ATTEQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name": "Ana Ruiz", "addresses": [{"kind": "phone", "value": "+34600123456"}]}'
```

A number already on another entry is refused with 409: one number is one person.
An entry made this way is someone the organization vouches for, with no account
and no role. Everyone with an account is on the roster already, with
`membership_id` set; that entry follows its account and no API call links,
renames or removes it. `update_roster_entry` renames an entry,
`add_roster_address`, `change_roster_address` and `remove_roster_address` change its numbers, and
`delete_roster_entry` stops vouching for somebody with no account, taking their
grants with them.

Then push their key:

```bash
curl -X PUT https://YOUR_PLATFORM_ORIGIN/api/v1/connections/$CONNECTION_ID/grants/$ROSTER_ENTRY_ID \
  -H "Authorization: Bearer $ATTEQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"grant_secret": "{\"header\": \"X-Api-Key\", \"prefix\": \"\", \"value\": \"ana-s-key\"}"}'
```

Calling it again replaces the key. `remove_person_grant` takes it away, and
`list_person_grants` says which entries hold one and whether each still works.
The secret is never returned.

## The words the API uses

| Concept | Operations | The fields that matter |
|---|---|---|
| Connection | `create_connection` | `provider` (`http_key` for the client's own system), `account_label`, `grant_secret` (for `http_key`: a JSON string `{"header", "prefix", "value"}`), `scopes` (empty for `http_key`), `granted_via: "pasted"`. The secret is never returned. |
| Roster entry | `list_roster`, `get_roster_entry`, `create_roster_entry`, `update_roster_entry`, `add_roster_address`, `change_roster_address`, `remove_roster_address`, `delete_roster_entry` | `display_name`; `addresses`, each `{kind: "phone", value}`, the value returned in international form; `membership_id`, set only on an account's own entry, which is never created, linked or removed through the API. |
| Grant per roster entry | `set_person_grant`, `remove_person_grant`, `list_person_grants` | On a connection created with `per_person: true`: the path names the `roster_entry_id`; the body is `grant_secret`, as on `create_connection`. The secret is never returned. |
| HTTP tool | `create_tool`, `test_tool` | `name`, `description`, `input_schema`, `config.endpoint`, `config.method`, `connection_id`. Never an auth header in `config.headers`: refused. |
| Built-in tool | `list_builtins`, `create_tool`, `update_tool` | `source: "builtin"` and `config.builtin` naming an entry of `list_builtins`, plus the `connection_id` that entry's `provider` needs. No `input_schema`: a built-in's parameters are the deployment's current ones, so an instance accepts what that built-in accepts today, whenever it was created. `fixed_parameters` holds what a person decided, hidden from the model; an `update_tool` that names it **replaces the whole set**. [Tools and their parameters](/tools-and-parameters) is the page for this. |
| Collection and its tools | `create_collection`, `create_record`, `update_collection`, `list_tools` | Fields with a type and a personal flag; `identity_field_key` set by `update_collection` once fields exist. The tools `query_<slug>`, `get_<slug>`, `write_<slug>` appear in `list_tools`, and their ids come back on the collection as `query_tool_id`, `get_tool_id`, `write_tool_id`. |
| Flow | `create_flow`, `add_step`, `add_flow_edge`, `set_entry_step` | A step is `{tool_id, param_overrides}`; an override value `"$ref:<step_id>.<path>"` reads an earlier step's output, `"$ref:trigger.<key>"` reads what the run was started with. An edge is `{source_step_id, target_step_id, condition: {field: "<step_id>.<path>", op, value}}` with ops `===`, `!==`, `>`, `<`, `includes`, `exists`. The first step added is the entry step. |
| Flow as a tool | `create_tool`, `test_tool` | `source: "builtin"` and `config.flow_id`, nothing else in `config`: no `endpoint`, no connection, because the platform runs the flow itself. `input_schema` is what the agent fills in; the steps read it as `$ref:trigger.<key>`. The resulting tool id is what nodes and hooks attach. [Tools and their parameters](/tools-and-parameters#a-flow-as-a-tool) has the body. |
| Automation | `create_automation`, `set_automation_enabled`, `run_automation` | `trigger_kind` is one of `schedule` (`trigger_config.cron`), `record_written` (`trigger_config.collection_id`), `interaction_ended` (`trigger_config.agent_id`, optional), `webhook`. `flow_id` names the work. Created disabled; enable it, or run it by hand. |
| Custom tool | `create_custom_tool`, `try_custom_tool`, `publish_custom_tool` | `purpose`, `source_code` with `def transform(arguments):` returning a value, `input_schema`, `output_schema`, `declared_reach: []`. Draft until published. `tool_id` on the response is the id a flow step or node attaches; in a flow the value it returned is read as `$ref:<step_id>.result.<key>`. |
| Attaching tools | `get_draft`, `add_node`, `update_node`, `set_root`, `edit_draft`, `publish_version` | `tool_ids` on a node; `global_tool_ids`, `pre_hook_tool_ids`, `post_hook_tool_ids` on the agent draft. A pre hook's `fixed_parameters` may name what the conversation opened with: `"$ref:conversation.number"` is the caller's number on an inbound call and the contact's on an outbound one (`update_tool`, whose `fixed_parameters` replaces the stored set whole: send the values you are keeping along with the one you are adding). A publish is refused unless the graph has a root node (`set_root`) and the draft names an `ai_profile_id`. Nothing takes traffic until the version is published. |

## Where next

- [Recipes](/recipes) show the operation sequences for the common jobs, each one
  run as a test against the API before it is published.
- [Tools and their parameters](/tools-and-parameters) — how to read what a
  built-in accepts, what a fixed parameter does, and why a `fixed_parameters`
  edit has to send the whole set.
- The [API Reference](/api) is complete and generated from the running app.
- [For agents](/agents) explains the MCP server and the machine-readable
  copies of these pages.
