An interaction function is a definition of the type interaction. It takes one of four shapes:
| Shape | What a run does | Use it for |
|---|---|---|
| Asks a system | Calls the tool named in call once, and returns part of the tool's result at once |
What a system does and answers now, such as adding a contact to a list |
| Asks a person through a connection | Renders a request to a party, with an expiry, and sends it through the tool of a connection named in deliver, such as a chat's post tool, and may read the replies to it |
An approval, a choice, or a figure only someone else has, where they work |
| Asks an agent | Renders the same request and holds it for an agent, deliver: agent, which reads it with list_interactions and answers it with answer_interaction |
The same, answered by the person in a conversation with their agent |
| Asks a person to sign in | Asks the person a run acts for to sign in to a site in a browser, with sign_in, and waits until they have |
A site the playbook's next steps use as that person |
A run that asks a person renders the message from its input. Then it waits until the request is answered, expires or is cancelled, without using server resources. The answer is checked against the document's answer schema, and becomes the run's output. A function without an answer schema sends a notification, which needs no answer and goes through a connection.
Write in deliver how the request reaches its party: the tool of a connection that sends to them, or agent. A person answers a request where it reaches them, so a request that names neither is refused when it is saved.
A connection carries a request when its server offers two tools:
- A tool that sends to a party. Its name says it sends, posts, publishes, replies, comments or mails, or creates or writes a message, a comment or mail, and names no other operation, such as update, delete, pin or list. It deletes nothing, and where an OpenAPI document describes it, that document does not say it answers without a body, since
sentpoints into what it answers. - A tool marked
readsthat reads the replies.
For example:
- a chat tool: the tool that posts a message in a channel or a thread, and the one that lists the replies of a thread;
- an email provider: the tool that sends mail, and the one that lists the messages of a thread;
- a ticketing or approvals app: the tool that adds a comment to an item, and the one that lists its comments.
deliver: agent holds the request for an agent: an agent the person talks to reads it with list_interactions, and answers it with answer_interaction once the person says what to answer.
A notification takes no answer, so no agent holds it: it goes through a connection, deliver naming the tool that sends it, and any connection with a tool that sends can carry it. Its run succeeds with the output {}, so nothing after it learns where the message landed. To post something whose replies a later step reads, write a function that asks a system instead: its call posts the message, and its output keeps the tool's answer, such as the message's ts, for a lookup function to read the replies by.
Ask the person once where they want to be asked. When a document names no way to reach its party, create_definition and update_definition refuse it, and the refusal names the ways this playbook has:
- For a request, each connection of the playbook whose tools can send a request and read its replies, with those two tools, and the agent way.
- For a notification that names no tool, or names
agent, each connection whose tools can send it.
A tool server of the server's own settings is named as a tool server. A playbook with none is told so, with the reason for each connection that cannot be used just now. Wherever no connection can carry it, the refusal says that one can be connected with search and add_connection, where the agent's tools include both.
When the agent's MCP client answers forms, the server itself asks the person before it refuses, in words such as "Where should the owner be asked?", or "Where should the owner be told?" for a notification. The person chooses from each such connection and, for a request, the agent way. Once they choose, the refusal names the connection's sending tool and, for a request, its reading tool, or says to write deliver: agent.
The agent then learns what each tool takes with introspect. Once the person agrees, it calls the sending tool with execute_connection and a message the party would want anyway, to see what the tool answers for sent. Then it writes deliver from that answer. For a request, it also calls the reading tool on a thread that already has replies, and writes replies from what it lists, as Sending through a tool sets out.
A request waits on a member of the org when its party, its to as rendered, is an email address, one that holds an @: it waits on the member whose address that is, compared without regard to case. A party that is a person's id, person: and an id, such as a sign-in's, waits on that person. Any other party, such as a channel, a role or a name, waits on any member of the org, whether the request goes through a connection or is held for an agent. Whom a request waits on does not limit who may answer it: any caller that may write to the playbook can answer, as Answering a request says.
So write to: "{{ input.reviewer }}", given the reviewer's address such as priya@acme.example, for a request one person should see as theirs. A request answered by reply takes a reply only when the reading tool names its sender exactly as the answerer is written: from, or to when there is no from. So a chat request that waits on an address also writes from as the chat names the person, and gives the sending tool the conversation to post in from the input rather than from to; see Answering by reply. The organization's activity shows each open request as a needs_you row whose waits_on holds the address in lower case, or any_member, and counts it for the caller it waits on.
The source is Markdown with YAML front matter followed by the message. This example, approve-campaign-brief, asks a campaign's owner, in the team chat, to approve the review of their brief, and takes their answer from their reply in the thread of the message. Its answer is { verdict: approve | revise, note? }:
---
description: Asks the campaign owner, in the team chat, to approve a reviewed brief
to: '{{ input.owner }}'
expires: P2D
deliver:
server: chat
tool: post_message
with:
channel: '{{ to }}'
text: '{{ message }}'
sent:
conversation: /channel
id: /ts
replies:
conversation: '{{ sent.conversation }}/{{ sent.id }}'
tool: thread_replies
with:
channel: '{{ sent.conversation }}'
ts: '{{ sent.id }}'
oldest: '{{ since | default: "0" }}'
limit: 15
read:
list: /messages
order: oldest_first
each: { id: /ts, sender: /user, text: /text, to: /thread_ts }
wait: PT1M
tell:
with:
channel: '{{ sent.conversation }}'
thread_ts: '{{ sent.id }}'
text: '{{ message }}'
reply: { verdict: word, note: rest }
input:
schema:
type: object
required: [owner, review]
properties:
owner: { type: string }
review: { type: string }
output:
schema:
type: object
required: [verdict]
properties:
verdict: { type: string, enum: [approve, revise] }
note: { type: string, maxLength: 2000 }
page:
data:
language: typescript
source:
events:
- type: run_started
subject: interaction/approve-campaign-brief
- type: run_succeeded
subject: interaction/approve-campaign-brief
- type: run_rejected
subject: interaction/approve-campaign-brief
- type: run_failed
subject: interaction/approve-campaign-brief
views:
waiting: categories
decisions: table
module: |
export function fold(view: View, event: Event): View {
const fieldOf = (value: Json | undefined, name: string): Json | undefined =>
typeof value === 'object' && value !== null && !Array.isArray(value) ? value[name] : undefined;
const open = (view.waiting?.values[0]?.value ?? 0) + (event.type === 'run_started' ? 1 : -1);
const waiting = { values: [{ name: 'Waiting for an answer', value: Math.max(open, 0) }] };
const verdict = fieldOf(fieldOf(event.data, 'output'), 'verdict');
const answer = verdict === 'approve' ? 'Approved' : verdict === 'revise' ? 'Sent back' : undefined;
const day = event.time?.slice(0, 10);
if (answer === undefined || day === undefined) return { ...view, waiting };
const rows = view.decisions?.rows ?? [];
const counted = rows.some(([at, kind]) => at === day && kind === answer)
? rows.map((row) => (row[0] === day && row[1] === answer ? [day, answer, Number(row[2]) + 1] : row))
: [...rows, [day, answer, 1]];
return { waiting, decisions: { columns: [{ name: 'Day' }, { name: 'Answer' }, { name: 'Briefs' }], rows: counted.slice(-60) } };
}
result: |
{% if run.status == "started" %}
Waiting for the campaign owner to answer in the team chat.
{% else %}
{% if output.verdict == "approve" %}Approved{% else %}Sent back{% endif %} by the campaign owner on {{ run.ended | slice: 0, 10 }}{% if output.note %}: {{ output.note | clip: 160 }}{% else %}.{% endif %}
{% endif %}
runs: |
```chart figure
{{ views.waiting | json }}
```
```chart bars x=Day y=Briefs color=Answer
{{ views.decisions | json }}
```
---
The review of your campaign brief is ready:
{{ input.review }}
Reply **approve**, or **revise** with a note of what to change.To save it, call create_definition with type: "interaction", the name approve-campaign-brief, the document as source, and its overview and diagram. A save without a diagram is refused, so give the word none when a picture would add nothing; The overview says how to write both. To run it, call run_definition with the same type and name, and with owner and review in the input object. Both operations also take the playbook id, unless the MCP connection is scoped to that playbook.
The run returns status: started with its run_id, and sends the review to the owner through the chat's post_message. The owner answers by replying in the thread of the message, or tells their agent, which answers with answer_interaction. The same request held for an agent writes deliver: agent in place of deliver, replies and reply. The function's page heads each run with the owner's answer and the day it came, and counts the requests still waiting and the answers of each day; see Its page.
A function that asks a system names a tool in call, with the arguments the call takes. Its run makes one call and takes the part of the tool's answer that read points at. It checks that part against output.schema and ends with it as its output.
A tool marked reads belongs to a lookup function, which takes the same call: an interaction function that names one is refused when it is saved and its server can be asked, in words that say to save it with the type lookup. See Lookup function format.
Such a function makes no request, sends no message and asks no model. So it has no to, expires, deliver, replies or body.
Write a call from what the tool returns, not from memory:
-
Call
searchto see the tool servers this playbook may use, the tools each offers and the mark of each. Callintrospectfor theinput_schemaa tool takes. -
introspectgives what a tool takes, not what it answers, and a call may change something. So look for a call that changes nothing: a tool that takes each id once changes nothing when it is called again with an id it has already taken. Once the person agrees, call it withexecute_connection, with the arguments itsinput_schematakes, and read itsanswer. That is the documentreadpoints into: the tool's structured content, else its first text block parsed as JSON, else that text. -
Write
call:serverandtool, assearchnames them;with, the arguments;read, a JSON Pointer to the part of the answer the function returns, such as/messages. Leavereadout to return the whole answer.
Then write
output.schema: the shape that part has in the answerexecute_connectiongave. Name only the fields the function's readers need, list inrequiredonly the fields every answer holds, and allownullwherever the tool may answernull: one answer does not show every case. -
Show the person the whole document. Once they agree, save it with
create_definition,typeinteraction.run_definitionruns it and returns its output at once.
The arguments in with are the tool's own, named as its input_schema names them, and may nest:
- A number,
true,false,null, or a list or an object of them, is sent as written. - A text is a Liquid template that reads
input,todayandnow, and is sent as the text it renders. - A text that is one
{{ }}and nothing else sends the value it reads, as that value is. So'{{ input.limit }}'sends a number when the input'slimitis a number.
read names a place in the answer and nothing more. To reshape what a call returns, use the workflow's output.as, or a computation function.
How a call ends when it goes wrong:
- Before anything is sent: when the playbook may not use the tool server, the operator does not allow the tool, or the server does not list it, the run ends
unavailableastool_not_offered, in words that namesearch. When the server cannot be reached, it endsunavailableastool_server_failed. Either can be run again. - Once the call is sent, it is in the run's history. When the tool server fails on it, or the tool returns an error:
- a tool marked
readsends the rununavailableastools_unfinished, status 503. A workflow may try it again. - any other tool may have changed something, so the run ends
conflictaseffect_unknown, status 409. A workflow's retry on status 503 does not match it.
- a tool marked
- The tool refused the arguments, or the answer holds nothing where
readpoints, or does not matchoutput.schema. The run endsconflictasunworkable, which trying again does not change.
A tool that pages returns one page for each run. Point read at the part that holds both the items and the cursor, and have the workflow call again with the cursor while there is one.
A run that called its tool and did not succeed is never run again under its run id, since the tool may have changed something. A request with that id returns conflict with the kind tools_called. Check the run's history, then start a new run under a new id. Retrying under a run id gives every case.
A function sends its request through a tool by naming the tool in deliver, with the arguments the call takes. It needs nothing set up beyond the tool server itself, a connection of the org or a server whoever runs this server set up.
Write a delivery from what the tool returns, not from memory:
-
Call
searchto see the servers this playbook may use and the tools each offers, andintrospectfor theinput_schemaa tool takes. -
The sending tool sends a real message, so wait until the person agrees, and make it one the party would want anyway, such as one that tells them the playbook will ask them here. Then call it with
execute_connection, with the arguments itsinput_schematakes, and read its answer. That is the documentdeliver.sentpoints into.sentholds two JSON Pointers: to the conversation the message landed in, and to the message's identity. A chat's tool that posts a message returns the conversation it posted in and the message's timestamp or id. -
If the person will answer where the message reached them, call the tool that reads what came since a point, with
query_connectionwhere it is markedreadsandexecute_connectionwhere it is not. Read a thread that already has replies, to see their fields without asking the party to reply. Then writereplies:conversation, the key the playbook reads the conversation by;toolandwith, the reading tool and its arguments, oversentand the cursorsince;read, the pointers to the list of replies and, within each, to its id, its sender, its words and the message it answers;wait, the floor of the reading cadence;tell, how the party is told when a reply was not an answer.
The
replyrule maps the words of a reply to the answer; see Answering by reply. Tool blocks gives each key, whether it is required and its default. -
Write
deliverwithserver,tool,withandsent. Show the person the whole document, and save it withcreate_definition,typeinteraction, once they agree. A run refuses a server this playbook may not use, or a tool its operator does not allow, in words that namesearch.
The arguments of deliver.with, replies.with and tell.with are typed as written:
- A number, boolean, null, list or object is sent as written, with the templates inside its strings rendered.
- A string that is exactly one
{{ expression }}is sent as the value the expression reads. A number stays a number and an object stays an object, so'{{ answer_schema }}'sends the schema itself. - Any other string is rendered as text. Use
| jsonto embed a structured value in it.
The templates of deliver.with read:
input,todayandnow, as the message does. Heretodayandnoware the request's own moment, so every attempt sends the same arguments.- The request's
to,message,run_id,function,expires_atandanswer_schema.tois the party the request goes to, which the templates may use as the tool's address or not.
A template holds no ${, since a document is never filled from the environment.
approve-campaign-brief sends the review to its owner in a chat, and takes their approval from the replies in the thread of the message. Every name in it is the chat's own, learned by testing its two tools. Testing shows what the sending tool takes, where its result names the message, and what the reading tool lists. channel is the chat's word for a conversation; the playbook passes it on as it passes any argument.
tellnames notool, so a party whose reply was not an answer is told throughpost_message, the sending tool, in the thread of the message.replytakes the verdict from the first word of a reply and the note from the rest. Without it, the rule made from the schema would read the verdict alone and drop the note.
Tool blocks gives every key of deliver and replies, with its default.
A request sent through a tool is attempted at once. After a failed attempt it is tried again 1, 2, 4 and 8 minutes after the one before: five attempts in all.
- A server that responds with
Retry-Afteris waited for as long as it asks, up to 8 minutes. - Each attempt is one call of the tool. It is recorded in the run's history before it is sent, with the size and digest of its arguments. So two servers never make the same attempt. Its end records what the tool returned.
- These are failed attempts, tried again: a tool error, a server failure, no response within 30 seconds, and a connection not opened within 10 seconds.
- Arguments past 16 KiB are refused and not tried again.
- A tool the server no longer offers to the playbook fails the attempt. The request stays open, and
answer_interactionanswers it. - An attempt cut off by a restart counts as failed a minute after it started, and the next one follows. So a tool may be called twice for one request.
- A question whose attempts are spent stays open until someone answers it or it expires. A notification whose attempts are spent ends
unansweredasundelivered.
| Field | Purpose |
|---|---|
description |
Optional explanation, 1 to 1,000 characters |
call |
The tool the function asks: server, tool, with and read; see call. With it, output.schema is required and to, expires, deliver, replies, from, reply and the body are refused |
sign_in |
The site the person a run acts for signs in to, server, site and if_signed_out; see Asking a person to sign in |
to |
Required without call or sign_in, refused with them. A template that renders the party the request goes to: a name the playbook's readers filter by, which a delivery's templates may use as the tool's address |
from |
Optional. A template, as to is, that renders the party whose reply counts, the answerer; to when left out. A request sent to a room names the person here |
expires |
Required without call, refused with it. An ISO 8601 duration from one minute to thirty days, such as PT4H or P2D, after which a request nobody answered ends |
deliver |
The tool the request is sent through: server and tool as search names them, and with, the call's arguments. sent holds two JSON Pointers into the tool's answer, to the conversation and to the message's identity; see deliver. Or agent, for a request held for an agent. Required without call |
replies |
Only with the tool of deliver, its sent and output.schema: how the replies to the message are read, with conversation, tool, with, read, wait and tell; see replies |
input.schema |
Optional JSON Schema of the input |
output.schema |
Optional JSON Schema of the answer. The answer must match it and is the run's output. A function without it is a notification: it takes no answer and succeeds once its request is delivered |
reply |
Optional, only with output.schema: the reply rule; see Answering by reply |
page |
Optional page that presents its runs |
model, config, tools, language and every other field are rejected with their line. The body after the front matter is the message. The saved function has the media_type text/markdown.
Quote a text that holds #, or a comma inside { }, such as description: 'Posts the notes in #deliveries' or { note: 'Costs 1,000 a month' }. Unquoted, YAML reads # as the start of a comment and a comma there, even the one in 1,000, as the start of another key, so the text ends early, and the save does not always say so.
| Field | Asks a system | Asks a person through a connection | Asks an agent |
|---|---|---|---|
call |
Required | Refused | Refused |
to, expires, the message |
Refused | Required | Required |
from |
Refused | Optional | Optional |
deliver |
Refused | Required: the tool that sends the request | Required: agent |
replies |
Refused | Optional; needs deliver.sent, output.schema and a reply rule |
Refused |
output.schema |
Required | Optional; without it, a notification | Required |
reply |
Refused | Optional, with output.schema; read only with replies |
Optional, with output.schema; nothing reads it |
description, input.schema |
Optional | Optional | Optional |
A function with sign_in takes the fields Asking a person to sign in lists. A request that names no deliver is refused when saved, and so is a notification that names no tool, whether it leaves deliver out or writes deliver: agent; How a request reaches its party says what the refusal names.
A function that reads replies needs a reply rule: the one reply writes, or one made from the answer schema. Without either, it is refused when saved.
to, from and the message are Liquid templates that read input, today and now and nothing else. A rendered value is text and is never read again as a template.
A template may use the filters listed in Templates, the templates guide, the same in every template.
Every template is checked when the document is saved, against the names its place reads:
| Place | Names it reads |
|---|---|
to, from, the message, call.with |
input, today, now |
deliver.with |
input, today, now, and the request's to, message, run_id, function, expires_at and answer_schema |
replies.conversation |
to, sent.conversation, sent.id |
replies.with |
to, sent.conversation, sent.id, conversation, since |
tell.with |
to, sent.conversation, sent.id, message |
A template is rejected with its line when it does not compile, names another value, or names a property the input schema does not allow. So is a structure inside a longer text, in a template that reads no input; one that reads input is checked when the run renders it.
Other rules checked when saved:
- Each pointer of
call.read,deliver.sentandreplies.readis a JSON Pointer such as/messages. It starts with a slash, writes~as~0, and writes a slash within a name as~1. read.orderisoldest_firstornewest_first.waitlies betweenPT5SandPT1H, and isPT5Swhen left out.- The server and the tool are not checked when the document is saved, but for the mark of the tool of
callwhen its server can be asked: one it marksreadsis refused, since it belongs to a lookup function. A document whose server cannot be asked just then, or does not serve the playbook, is saved. A run whose server does not serve the playbook, or whose tool its operator does not allow, isunavailablewith the kindtool_not_offered.
A request uses the tools of one server, deliver.server:
| Step | Tool |
|---|---|
| Send the message | deliver.tool |
| Read the replies | replies.tool |
| Tell the party how to answer | replies.tell.tool, or deliver.tool when tell names none |
A function with call uses call.tool on call.server. A run checks every tool its function names before it sends anything: the server must serve the playbook, and the operator must allow the tool. Otherwise the run is unavailable with the kind tool_not_offered.
| Key | Required | What it holds |
|---|---|---|
server |
Yes | The tool server, as search names it: 1 to 32 lowercase letters, digits and hyphens, starting with a letter |
tool |
Yes | The tool, as search lists it: 1 to 128 letters, digits, underscores, hyphens and dots. The server must list it |
with |
No; {} when left out |
The tool's arguments, named as its input_schema names them. Their templates read input, today and now |
read |
No; the whole answer when left out | A JSON Pointer to the part of the tool's answer the run returns, such as /messages. An empty pointer is refused |
The answer is the tool's structured content, else its first text block parsed as JSON, else that text.
| Key | Required | What it holds |
|---|---|---|
server |
Yes | The tool server, as search names it. Reading replies and telling use it too |
tool |
Yes | The tool that sends the message |
with |
Yes | The sending tool's arguments. Their templates read input, today, now and the request's to, message, run_id, function, expires_at and answer_schema |
sent |
With replies; otherwise no |
Where the tool's answer names the message it sent; see deliver.sent. Without it, the history does not say what the message was delivered as |
| Key | Required | What it holds |
|---|---|---|
conversation |
Yes | A JSON Pointer to the conversation the message landed in, such as /channel |
id |
Yes | A JSON Pointer to the message's identity, such as /ts |
- Each must point to a text or a whole number of at most 256 bytes, with no control character.
- When one does not, the request is still delivered, without what it was delivered as. A function that reads replies then takes no reply, and the request waits for
answer_interaction. - The templates of
repliesandtellread the two values assent.conversationandsent.id.
| Key | Required | What it holds |
|---|---|---|
conversation |
No; '{{ sent.conversation }}' when left out |
A template over to, sent.conversation and sent.id that renders the key the playbook reads the conversation by. Open requests with the same reading tool and key share one reading |
tool |
Yes | The tool of deliver.server that lists the messages of a conversation since a point |
with |
Yes | The reading tool's arguments. Their templates read to, sent.conversation, sent.id, conversation and since |
read |
Yes | Where the replies are in the reading tool's answer; see replies.read |
wait |
No; PT5S when left out |
The shortest time between two reads, from PT5S to PT1H |
tell |
No; nobody is told when left out | How the party is told that a reply was not an answer; see replies.tell |
- A key that renders empty, past 256 bytes or with a control character leaves the request to
answer_interaction. - In
with,conversationis the rendered key.sinceis the id of the newest message an earlier read listed, and empty text before any. Write'{{ since | default: "0" }}'where the tool needs a value. - A conversation that several open requests share is read with the
toandsentof the oldest of them.
| Key | Required | What it holds |
|---|---|---|
list |
Yes | A JSON Pointer to the list of messages in the reading tool's answer, such as /messages |
order |
Yes | oldest_first or newest_first: the order the tool lists them in, which tells which is newest |
each |
Yes | Pointers within each message; see replies.read.each |
An answer with no list at list, or larger than 64 KiB, is a failed read. The reading goes on.
| Key | Required | What it points to, within each message |
|---|---|---|
id |
Yes | The message's identity. The newest message's becomes since |
sender |
Yes | Who wrote it. Only a message from the answerer counts |
text |
Yes | Its words, which the reply rule reads. A message with nothing there reads as empty and is refused |
to |
No | The message it replies to, such as the first message of its thread |
- A message whose
idorsenderis not a text or a whole number of at most 256 bytes, without a control character, is passed over. - A message whose
idis one the playbook sent in the conversation, such as the first message of a thread it started, is the playbook's own and not a reply. It is passed over and not counted, so a read that finds nothing else has found nothing and records noreplies_read. - With
to, a message answers the open request whosesent.idit names. It is passed over when it names none of them. - A message with nothing at
to, or any message wheneachhas noto, answers the conversation's one open request. It is refused when several are open.
| Key | Required | What it holds |
|---|---|---|
tool |
No; deliver.tool when left out |
The tool of deliver.server that sends the telling |
with |
Yes | Its arguments. Their templates read to, sent.conversation, sent.id and message |
- Here
messageis the telling, not the request's message. It says how to answer, such asTo answer, reply with one of approve, revise; what follows is kept as the note.After a reply the answer schema refuses, it names the problems first. After a reply that answers no single request, it asks the party to reply to the message of the one they answer. After a reply past 8 KiB, it gives the limit. - A request is told at most three times. Arguments that do not render are not sent; the reply is still refused.
answer_interaction answers a request over MCP. Over HTTP it is POST /v1/orgs/{org}/playbooks/{playbook}/runs/{run_id}/answer. It takes the run_id of the interaction function's run, which list_interactions shows, and the answer:
{ "answer": { "verdict": "approve", "note": "Ready to launch." }, "claimed_for": "the campaign team" }A person often answers in a conversation with their agent: they approve, reject, ask for changes, or say it in other words. The agent then calls answer_interaction on the request's run_id, with the answer in the shape of the request's answer_schema:
list_interactionsshows theanswer_schemabeside therun_id. It is the function'soutput.schemaas it stood when the request was asked. Once the function has changed,get_definitionno longer shows it.- For the document above, the person's "approve" is
{ "verdict": "approve" }, and "revise: name the job role" is{ "verdict": "revise", "note": "Name the job role." }. A function with anotheroutput.schematakes its own shape, whatever words the person used: for a function whose answer has adecisionofapprove,reviseorskipand an optionalnote, it is{ "decision": "approve" }. - This holds for a request held for an agent and for one sent through a tool, such as a chat's.
- Running the function or its workflow again answers nothing: it makes a new request, the first stays open until it expires, and the run that waits for it goes on waiting.
Rules of an answer:
- A caller that may write to the playbook can answer, but for a sign-in request, which the person it asks answers alone.
- The answer is checked against the answer schema the request recorded, and takes at most 64 KiB as JSON.
- It is recorded on the run as one fact,
request_answered, with the answer andclaimed_for.claimed_foris whom the caller says it answers for: a claim the runtime keeps and never checks. The fact records the caller and the time itself. answer_interactionreturns that fact: therun_id, theanswer_idof the fact (whichget_eventreads), theanswer,claimed_for,answered_by(the caller who answered, byidandname) andanswered_at.- The request leaves the open requests at once. The run takes the answer within about a second (the server checks waiting runs every second by default). It succeeds with the answer as its output, settled as the caller that answered.
- An answer that does not match is
invalid_input, with a pointer under/answerfor each problem, and leaves the request open. - The same answer sent again returns the fact the run took. A different answer is
conflict, since a run takes one answer. - An answer to a request that has ended, or whose run was asked to cancel, is
conflict, and so is an answer to a notification.
list_interactions, or GET /v1/orgs/{org}/playbooks/{playbook}/interactions over HTTP, lists the playbook's open requests, newest first. An answered request leaves the list. Each request shows:
- the
run_id, and thetype, the name and the version of the function that asked; to, and thedelivery: the tool the request is sent through, ornullfor a request held for an agent;- the message, whether it takes an answer, and when it was asked and expires;
- the attempts made, and how its delivery stands:
held_for_an_agent,to_deliver,delivering,delivered,retrying,undelivered, orcancellingonce a cancel of its run was asked; - its
answer_schema, the JSON Schema an answer is checked against as the request recorded it, ornullfor a notification; - for a request answered by reply, the
conversationthe playbook reads, itsanswererand how many of its replies were refused; - for a sign-in request, its
sign_in, with theserver, thesiteand thelinkwhere the person signs in, andnullfor every other request.
Filters:
tokeeps the requests to one party;typekeeps those of one type of function;functionkeeps those of functions of one name. A name is unique within its type, sofunctionwithtypenames one function.
The list pages with limit and cursor. Every reader of the playbook sees each party and message, as a run's input is seen.
Where a function reads replies, the person answers by replying to the message the playbook sent, in the chat where it reached them. While the request stands open, the playbook reads the conversation's new replies:
- every few seconds at first, and every five minutes at most, never more often than the function's
wait; - taking a reply only from the answerer: the function's
from, ortowhen it gives nofrom. A reply from anyone else is passed over and kept nowhere.
The reply rule reads the answer from a reply. It maps each top-level string property of output.schema to where in the reply its value comes from:
| Part | The value is |
|---|---|
word |
The reply's first word: one of the property's enum values, or a word that words lists for one |
rest |
What follows the first word, such as a note |
text |
The whole reply |
A part is written as word, rest or text, or as an object with from and, for word, words. For example, reply: { verdict: { from: word, words: { approve: [yes, ok] } }, note: rest } takes verdict from the first word and note from the rest. There "yes" also means approve. Words are compared without case and without punctuation around them. A word means one value only, and only word takes words. A notification takes no reply rule.
Left out, the rule is made from the schema when the answer has one required property that is a string. That property comes from the first word when it has an enum, and from the whole reply when it has none. The rule made this way reads no other property, so write reply to keep a note.
How replies are taken:
- A reply the rule cannot read is refused. Where the function writes
tell, the party is told how to answer, three times at most. - A reply in a thread answers the question the thread began with. A reply outside a thread answers the conversation's one open request, and is refused when several are open.
- The answer is recorded as any answer is, as
request_answered. It carriesreply, the identity of the message that answered. It names the playbook (playbook:and its id) as the one that answered. The run takes it at once. - A reply taken once is never taken twice, and a request already answered takes no further reply.
- A request whose function reads no replies is answered with
answer_interactionalone. A function that reads replies always has a reply rule, since one without is refused when saved.
| Ending | When |
|---|---|
succeeded |
The request was answered, and the answer is the output |
succeeded |
A notification was delivered, with the output {} |
succeeded |
A function with call: the tool returned a result and the part read points at matched output.schema; that part is the output |
invalid_input |
The input does not match the input schema, a template names a value the input lacks, or a filter refuses a value it reads, as money refuses text that is not a decimal number: The message cannot be rendered with this input |
conflict, kind unworkable |
to or from rendered empty, as something that is not text, with a control character, or longer than 256 bytes |
conflict, kind unworkable |
The message is longer than 8 KiB, or the arguments of deliver pass 16 KiB or one renders no text |
conflict, kind unworkable |
A function with call: an argument renders a structure among text, or the arguments pass 16 KiB |
conflict, kind unworkable |
A function with call: the tool refused the arguments |
conflict, kind unworkable |
A function with call: the answer has nothing to read, or nothing where read points |
conflict, kind unworkable |
A function with call: the part at read is more than a run may record (1 MiB with its record), or holds a value output.schema refuses |
unavailable, kind tool_not_offered |
A server the function names does not serve the playbook, or its operator does not allow a tool the function names. For a function with call, also a tool the server does not list. Nothing was sent |
unavailable, kind tool_server_failed |
A function with call: the tool server could not be reached, or refused the connection or the listing of its tools. Nothing was sent |
unavailable, kind tools_unfinished |
A function with call: the call was sent and then failed, and the tool is marked reads. Trying again under a new run id is safe |
conflict, kind effect_unknown |
The same, for a tool not marked reads, which may have changed something. No retry on status 503 matches it; a person, or a workflow rule that names the kind, decides |
unavailable, kind requests_full |
The playbook already has as many open requests as the runtime allows: 10,000 unless its operator sets another number with INTERACTION_OPEN_REQUESTS |
rejected as unanswered, kind expired |
Nobody answered the request before it expired |
rejected as unanswered, kind undelivered |
Every attempt to deliver a notification failed |
rejected as unanswered, kind declined |
The person a sign-in asked answered that they did not sign in |
rejected as unanswered, kind signed_out |
A sign-in with if_signed_out: fail found its person not signed in |
rejected as unanswered, kind no_person |
A sign-in acts for no person, or for one who has left the organization |
rejected as cancelled |
cancel_run cancelled the run, or the workflow that waited for it ran out of time or ended first |
failed |
The runtime itself broke down |
More rules:
- A failed call has a
because. It isserver_failedwhen the server failed on the call or did not respond within 30 seconds. It istool_errorwhen the tool returned an error. Bothtools_unfinishedandeffect_unknowncarry it. - After a call of a tool that may change something, an answer the run cannot use still ends
unworkable. Its detail says that running again calls the tool again. - A request that has ended is final for its run id. Answering it is
conflict, and running the function again starts a new request under a new id.
The run's history shows, in order:
interaction_requested: the delivery, the party, the size of the message, whether it takes an answer, and the expiry.- Each delivery attempt as
delivery_started, with the tool and the size and digest of its arguments, and then how it ended:delivery_succeeded, with the size and digest of what the tool returned. Where the delivery writessent, it also shows what the message was delivered as and where its replies are read.delivery_failed, withbecause(tool_error,arguments_refused,server_failure,timed_out,too_largefor an answer larger than one call may take,tool_not_offeredorlost), its words, and the wait a server asked for asretry_after_ms.delivery_refused, withbecausetoo_largeorunworkable.
- The answer as
request_answered, with the answer, the claim and, for a reply, its identity. - Each refused reply as
reply_refused, with the reply's identity and never its words. - The run's end.
A function with call shows its call as tool_call_started and tool_call_answered, or tool_call_failed when the tool never returned a result.
What is kept and shown:
- The playbook keeps the arguments, the message among them, and the tool's whole result, scrubbed of secrets. The operator can turn this off for a server with
record_content. - The history shows each where it takes at most 2 KiB, and its size beyond.
get_eventreads one event with both whole. - An answer is the run's output. Its
run_succeededshows it where it takes at most 2 KiB, andget_runreturns it whole. list_playbook_eventsshows the playbook's reads of a conversation,replies_readorreading_failed, and its tellings:telling_started, thentelling_succeededortelling_failed.
| Bound | Value | When it is reached |
|---|---|---|
| The rendered party, and the answerer | 256 bytes, no control character | conflict, unworkable |
| The rendered message | 8 KiB | conflict, unworkable |
| The arguments of a call | 16 KiB as JSON | conflict, unworkable at the start; a read or telling is not sent |
The arguments of a call that reads no input |
16 KiB as JSON | Refused when saved |
| A call of the tool | 10 seconds to open, 30 seconds to respond | A failed attempt or read |
| A function's call of its tool | 10 seconds to open, 10 to list the tools, 30 to respond | tool_server_failed before sending; then tools_unfinished or effect_unknown |
What a function's read takes |
1 MiB as JSON with the run's record, 512 levels | conflict, unworkable |
Calls in one run of a function with call |
1, within 70 seconds | Not applicable |
What sent reads, and the conversation key |
256 bytes of text each, no control character | Delivered without what it was delivered as; the request takes no reply |
| A read's result | 64 KiB | A failed read; the reading goes on |
| A reply's words | 8 KiB | The reply is refused |
| A reply rule | 16 values, 16 words a value, 64 bytes a word | Refused when saved |
| Refused replies of a request | 10 recorded, 3 told | Further ones leave no trace, or are recorded and nobody is told |
| Reads of a conversation | 5 seconds after a request joins, then 10, 20 and 40 seconds, then every minute through the 18th read, then every 5 minutes | Not applicable |
expires |
1 minute to 30 days | Refused when saved |
| An answer | 64 KiB as JSON, 512 levels | invalid_input |
claimed_for |
256 bytes | invalid_input |
sign_in.site |
A host name of 253 bytes, lower case, with no scheme, path or port | Refused when saved |
The reason of an answer to a sign-in |
500 characters | invalid_input |
| Attempts | 5: at once, then after 1, 2, 4 and 8 minutes | A question stays open; a notification ends unanswered as undelivered |
| Open requests of a playbook | 10,000 unless the operator sets another number | unavailable, requests_full |
Reads of a conversation never come sooner than the function's wait. A server that responds to a read with Retry-After is waited for as long as it asks, up to an hour.
A workflow calls an interaction function as it calls any function, with call: run_definition and type: interaction.
A function with call finishes within its call:
- The step's output is what the tool returned at
read. The function's own maximum is 70 seconds, so the step waits at most 70 seconds plus one minute. - A 503 it raises,
tool_server_failed,tool_not_offeredortools_unfinished, is safe to try again, so a retry on that status matches it. effect_unknownraises 409 under the problem typehttps://on.auto/problems/effect_unknown. A retry on status 503 leaves it alone. Acatchthat names its type or kind, matches status 409 or catches every error takes it, and a retry then calls the tool again under a new run id.
A function that asks a person:
- The step waits up to the function's
expiresplus one minute for the request, across a restart and without using server resources. Its output is the answer. - A request nobody answers raises an error of the problem type
https://on.auto/problems/unanswered, status 410, with the kindexpired,undeliveredfor a notification, or for a sign-indeclined,signed_outorno_person. - Only a
catchthat names that type or status 410, or catches every error, takes it. So a workflow handles an unanswered request only on purpose. A workflow that does not catch it endsrejectedasunansweredwith the same kind. - When the step runs out of time, or the workflow ends first, the request is cancelled.
This workflow asks for the approval and, when nobody answers in time, asks for a revision:
document:
dsl: '1.0.3'
namespace: campaign-review-tutorial
name: brief-approval
version: '1.0.0'
summary: Asks the campaign's owner to approve a review, and asks for a revision when nobody answered.
input:
schema:
document:
type: object
required: [owner, review]
do:
- approval:
try:
- ask:
call: run_definition
with:
type: interaction
name: approve-campaign-brief
input: '${ $data }'
catch:
errors:
with: { type: https://on.auto/problems/unanswered, kind: expired }
do:
- unanswered: { set: { verdict: revise, note: No answer within two days } }The run ends succeeded with the answer, such as {"verdict": "approve"}, as its output, or with {"verdict": "revise", "note": "No answer within two days"} when the request expired. list_interactions shows the request while it waits, under the run_id of the interaction function's run.
send_run_event remains for events a waiting workflow listens for that are not the answer to a question.
create_definition and update_definition take an overview beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs; less technical, more value. Write it after the document, and write it again with every update.
They also take a diagram, every time, so that whether the function is drawn is a choice its author makes: Mermaid source of a flowchart or a state diagram of how the function works, whose first line is flowchart, graph or stateDiagram, or the word none when a picture would add nothing. A save without it is refused. A diagram holds at most 8,192 bytes in UTF-8, and Auto Studio draws it under the overview. It is read there at full size in a column, so draw it top to bottom, as flowchart TD or a state diagram, in three to eight nodes, each a few plain words for the person who runs the business rather than an expression or code.
An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and get_definition returns it so until an update gives another; an update that leaves it out keeps the overview saved. list_definitions leaves it out. Reasoning functions gives the rules every definition follows, with used_by, the definitions of the playbook that use this one.
An interaction function's page is the page key of its front matter, written with the document and saved with it. It shows the person what was decided:
result: the decision, who made it and when, such asApproved by the campaign owner on 2026-11-01: Ready to launch.The answer isoutput, and the moment it camerun.ended.resultreads neither the input norto, so name who answers in words. Nor can it say what was decided on, such as which brief was approved: the answer holds only what the person answered, so the page of the workflow that asks says it, from the workflow's output. While the request waits,resultsays whom it waits for.runs: the decisions over time, what waits now, and how long people took to answer. A request's run starts withrun_startedand ends withrun_succeeded, the answer,run_rejectedwhen it ends without one, as when nobody answered, orrun_failed, each naming the run insource. So a fold counts each day's answers by their verdict, the requests still open, and the hours between asking and answering.
A fold pairs a request's start with its end by source. Another page for approve-campaign-brief, in place of the one in its document, keeps the requests still open in a view of its own, open, which holds each run's source and when it asked. runs never draws that view, so the run ids in it never reach the page; it draws the hours each answer took:
page:
data:
language: typescript
source:
events:
- type: run_started
subject: interaction/approve-campaign-brief
- type: run_succeeded
subject: interaction/approve-campaign-brief
- type: run_rejected
subject: interaction/approve-campaign-brief
- type: run_failed
subject: interaction/approve-campaign-brief
views:
open: table
hours: series
module: |
export function fold(view: View, event: Event): View {
const columns = [{ name: 'Request' }, { name: 'Asked' }];
const asked = view.open?.rows ?? [];
if (event.type === 'run_started') {
return { ...view, open: { columns, rows: [...asked, [event.source, event.time ?? null]].slice(-500) } };
}
const start = asked.find(([source]) => source === event.source)?.[1];
const open = { columns, rows: asked.filter(([source]) => source !== event.source) };
const points = view.hours?.lines[0]?.points ?? [];
const last = points.at(-1)?.at;
const time = event.time;
if (event.type !== 'run_succeeded' || typeof start !== 'string' || time === undefined) return { ...view, open };
if (last !== undefined && last >= time) return { ...view, open };
const hours = Math.round((Date.parse(time) - Date.parse(start)) / 360_000) / 10;
const line = { name: 'Hours to answer', points: [...points, { at: time, value: hours }].slice(-200) };
return { ...view, open, hours: { unit: 'hours', lines: [line] } };
}
runs: |
{% if views.open %}Briefs waiting for their owner's answer: {{ views.open.rows | size }}.{% endif %}
```chart line
{{ views.hours | json }}
```Show numbers over time as a chart keyed by the period a run is about, and anything else in its clearest form: a table, a list or a sentence. Show the value of the work, not telemetry: never how long the runtime took, a model's tokens or a run's id.
A notification takes no answer, so its page counts what was sent. Page format says how to write data, result and runs.
A function with sign_in asks the person a run acts for to sign in to a site in a browser, and waits until they have. A playbook that uses a site behind a sign-in has this step before the steps that use the site. The person signs in themselves, in a browser the browser connection hands them, and the agent, the runtime and the run's history never see what they type.
---
description: Makes sure the person the stock check runs for is signed in to the supplier portal
sign_in:
server: browser
site: portal.supplier.example
if_signed_out: ask
expires: P1D
deliver: agent
---
The weekly stock check reads your open orders on the supplier portal, and you're not signed in there.| Field | Purpose |
|---|---|
sign_in.server |
Required. The browser connection, as search names it, whose server offers browser_sign_in |
sign_in.site |
Required. The site's host name, such as portal.supplier.example: lower case, without a scheme, a path or a port, at most 253 bytes, never a template |
sign_in.if_signed_out |
Required. ask waits for the person; fail ends the run at once when they are not signed in |
expires, deliver and the message |
Required with ask and refused with fail, as for every request. The run adds the link where the person signs in on a line of its own after the message |
input.schema, description |
Optional |
to, from, call, replies, reply and output.schema are refused beside sign_in: the party is always the person the run acts for, the answer comes from their signing in, and the output is always { "signed_in": true }.
When the agent builds such a function, it asks the person three things: whether a run waits for them or stops at once, where to reach them, and how long to wait. A save that leaves out if_signed_out, or with ask leaves out deliver or expires, is refused with the line of the field. To a client that answers forms, the server asks the person first, one question at a time, as it asks where to reach a party.
A run of it:
- Finds the person it acts for: whoever started it, the local developer in local mode, or for a run a trigger started, its
acts_for, whoever saved or restored the version it runs (When a trigger applies). A run a workflow's step started acts for the person the workflow's run acts for. A run started with a key acts for nobody, and so does a triggered run of a version a key saved. With nobody, or a person who has left the organization, it endsunanswered, kindno_person, and asks nothing. - Checks that the server serves the playbook and offers
browser_sign_in, and withaskthe tool ofdeliver. Otherwise it endsunavailable, kindtool_not_offered, and sends nothing. - Calls
browser_sign_inonce, recorded as a function's call is: with the site, the request's address<playbook>/<run_id>anduntil, when the request expires, or withfailthe site andask: false. - Succeeds at once with
{ "signed_in": true }when the person is signed in already. Withfail, a person who is not ends the rununanswered, kindsigned_out. - With
ask, makes a request and waits, as every request waits.
The request waits on the person by id, such as person:user_7kLm2, as both its to and its answerer. list_interactions shows its sign_in: the server, the site and the link where the person signs in. The organization's activity lists it as a needs_you row whose waits_on holds that id, counted for that person alone.
Only the person it asks may answer it, and any other caller is refused forbidden. { "signed_in": true } is taken only from the person's own session, such as the one Auto Studio signs in with, and is refused forbidden through an agent's token. Their agent may answer { "signed_in": false, "reason": "..." } when they will not sign in, the reason at most 500 characters. In local mode the local developer may give either answer.
{ "signed_in": true } ends the run succeeded with that output. { "signed_in": false } ends it unanswered, kind declined, its detail naming the site and the reason. A request nobody answers ends expired, as every request does. So a workflow catches every way of not being signed in with one catch of https://on.auto/problems/unanswered, and carries on or fails as the playbook says.
The run's history shows the call of browser_sign_in, interaction_requested and request_answered. The answer records who answered and when, so it is the record that the person signed in to the site. Nothing of the sign-in itself reaches the runtime.
Once the function is saved, offer to sign the person in there and then, so the first run finds them signed in: once they agree, call the browser connection's browser_sign_in with execute_connection and the site, and give them the link it answers.
Interaction functions are available in every runtime. A request reaches a person through the tools of the tool servers the operator configures and of the org's connections, or is held for an agent. See Functions and availability.