Recall function format
A recall function keeps a view: a JSON value that its module folds from the playbook's events, one event at a time. The fold starts from the first event of the playbook's history. A run returns what the view holds as it stands. So the playbook can tell what it decided before, such as the reviews of each campaign, the latest verdict per region or the refusals of a quarter. A recall function is the playbook's memory: what a person wants kept so that the playbook can be asked about it again, shared by everyone who asks the playbook and by its workflows, and distinct from whatever notes an agent keeps for itself. The API stores a recall function as a definition of the type recall.
The view depends on the playbook's events alone:
- Nothing a run passes in is kept, and no run changes the view.
- The same history folds to the same view on every server and on either store.
- Nothing has to write into a recall function. The playbook's events already hold every run of each function and workflow: its start, and its ending, with its output when it succeeded and fit its event. They also hold the definitions saved and every event published to the playbook. So a view over a function's runs sees each run, whoever started it.
- The events a fold sees hold no run's input and no tool call, although the playbook's history records both; see The events. A view keeps what a run returned, never what it was given or what it called. For a view to keep what a function posted, the function returns what it posted.
A function document
The source is Markdown with YAML front matter, followed by the module. This example keeps, for each campaign, the reviews that the reasoning function review-brief wrote. It keeps the last 20 reviews of each of the 50 campaigns reviewed most recently, and a run returns the last few reviews of the campaign it names:
---
description: The reviews of each campaign, latest last, as the review-brief function wrote them
language: typescript
source:
events:
- type: run_succeeded
subject: reasoning/review-brief
view:
initial: {}
schema:
type: object
maxProperties: 50
additionalProperties:
type: array
maxItems: 20
items: { type: object, required: [at, verdict, run], properties: { at: { type: string }, verdict: { type: string }, run: { type: string } } }
input:
schema:
type: object
required: [campaign]
properties:
campaign: { type: string }
last: { type: integer, minimum: 1, maximum: 50 }
output:
schema:
type: array
items: { type: object, required: [at, verdict], properties: { at: { type: string }, verdict: { type: string }, run: { type: string } } }
---
type Review = View[string][number];
export function reviewsOf(view: View, campaign: string): Review[] {
const kept = Object.hasOwn(view, campaign) ? view[campaign] : undefined;
return Array.isArray(kept) ? kept : [];
}
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 lastAt = (reviews: Review[]): string => reviews.at(-1)?.at ?? '';
const output = fieldOf(event.data, 'output');
const named = fieldOf(output, 'campaign');
const campaign = typeof named === 'string' ? named.slice(0, 100) : 'unknown';
const given = fieldOf(output, 'verdict') ?? 'none';
const verdict = (typeof given === 'string' ? given : JSON.stringify(given)).slice(0, 60);
const reviews = [...reviewsOf(view, campaign), { at: event.time ?? '', verdict, run: event.source }].slice(-20);
const latest = Object.entries({ ...view, [campaign]: reviews })
.toSorted(([, first], [, second]) => (lastAt(first) < lastAt(second) ? -1 : lastAt(first) > lastAt(second) ? 1 : 0))
.slice(-50);
return Object.fromEntries(latest);
}
export function answer(view: View, input: Input): Output {
return reviewsOf(view, input.campaign.slice(0, 100)).slice(-(input.last ?? 5));
}View, Input and Output are the view, input and output schemas as TypeScript types. Event is the playbook's event and Json any JSON value. The runtime declares all of them for the module; see The module.
To save the function, create_definition takes type: "recall", a function name such as campaign-reviews, and the document as source. To run it, run_definition takes the same type and name, with an input object. The input holds the campaign, and optionally last, how many reviews to return. Both operations also require the playbook id unless the MCP connection is scoped to that playbook.
After three runs of review-brief, the view is:
{
"autumn-launch": [
{
"at": "2026-09-30T14:02:11.000Z",
"verdict": "approve",
"run": "/runs/0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b71"
}
],
"spring-sale": [
{
"at": "2026-10-01T09:15:42.000Z",
"verdict": "reject: the budget is not stated",
"run": "/runs/0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b72"
},
{
"at": "2026-10-03T16:40:05.000Z",
"verdict": "approve",
"run": "/runs/0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b73"
}
]
}Given this input:
{ "campaign": "spring-sale", "last": 1 }the run succeeds with this output:
[
{
"at": "2026-10-03T16:40:05.000Z",
"verdict": "approve",
"run": "/runs/0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b73"
}
]Guard the fold
The fold runs once for every run of review-brief that succeeds, whatever the model returned. So it guards against every output it may meet:
- An output that is not an object, such as a text or a list, counts for the campaign
unknown. So does a campaign that is not a text, such as a number. - A missing verdict reads
none. A verdict that is not a text, such as a number, reads as its JSON text. - An output too large for the playbook's event counts for
unknownwith the verdictnone, since the event then carries the output's size instead of the output. - An object keeps its keys in the order they were written. So the fold sorts the campaigns by the time of each one's last review, then keeps the last 50.
A fold that raises an error on an ordinary output stops its view at that event. So does a view that outgrows its bound or its schema; see When a view stalls. Guard the fold, and bound the view, before you save it.
Look up keys with Object.hasOwn
A key that comes from an event or an input can name a property that every object inherits, such as constructor, toString or __proto__. Then view[key] reads that inherited value instead of an entry of the view. A fold that spreads it into a list raises an error and stalls the view. An answer that calls slice on it ends its run conflict, unworkable.
So whenever a fold or an answer looks up a key that came from an event or an input, read the entry only when the view holds it itself:
reviewsOftakes a campaign's entry only whenObject.hasOwn(view, campaign)holds and the entry is a list. Otherwise the campaign has no reviews yet.- The fold writes the next view with a computed key,
{ ...view, [campaign]: reviews }, andObject.fromEntries. Both make even__proto__an entry of the view's own. An assignment such asview[campaign] = reviewswould change the object's prototype instead when the campaign is__proto__.
A campaign named constructor therefore starts with no reviews and keeps an entry of its own, and a run for it returns its reviews as for any other.
Bound the size of the view
A view may take 512 KiB as JSON: 524,288 bytes in UTF-8, checked after every fold. A view past that stalls at the event that made it too large. So bound every list and every text a fold keeps, then work out the largest view from those bounds. The example bounds its view this way:
- It keeps at most 20 reviews of a campaign and 50 campaigns, and drops the oldest beyond them.
maxItemsandmaxPropertiesinview.schemasay the same. - It cuts a campaign's name at 100 characters and a verdict at 60.
slicecounts UTF-16 code units, so an emoji takes two. JSON writes each unit in at most 6 bytes, as\u0000for a control character. - Auto writes a run's time and its address at a fixed length.
atholds 24 characters, such as2026-10-03T16:40:05.000Z, andrunholds 42:/runs/and the run's UUID. answercuts the campaign it is given at 100 characters too, so a run finds the entry of a longer name. Two names that agree in their first 100 characters share one entry. So do a campaign namedunknownand outputs that name no campaign.
The largest view, in bytes of JSON:
| Part | Largest size |
|---|---|
| A verdict | 60 × 6, and 2 quotes: 362 |
| A review | The verdict, the time (24), the run (42), and 29 bytes of field names and punctuation: 457 |
| A campaign's list | 20 reviews, 19 commas and 2 brackets: 20 × 457 + 21 = 9,161 |
| A campaign's name | 100 × 6, and 2 quotes: 602 |
| A campaign's entry | The name, a colon and the list: 602 + 1 + 9,161 = 9,764 |
| The view | 50 entries, 49 commas and 2 braces: 50 × 9,764 + 51 = 488,251 |
So the view takes at most 488,251 bytes, about 477 KiB, under the 524,288 a view may take. With ordinary names and verdicts it is far smaller. Bound any view the same way: multiply the largest entry by the most entries, add the punctuation, and keep the total under 524,288 bytes.
Keep what one function returned
The common case keeps what one function returned. This document keeps the output of each run of the reasoning function post-announcement that succeeds, the last 50, oldest first:
---
description: What the post-announcement function returned, oldest first, the last 50
language: typescript
source:
events:
- type: run_succeeded
subject: reasoning/post-announcement
view:
initial: []
schema: { type: array, maxItems: 50 }
---
export function fold(view: View, event: Event): View {
const bytesOf = (text: string): number => encodeURIComponent(text).replace(/%[0-9A-F]{2}/gu, '_').length;
const data = event.data;
const output = typeof data === 'object' && data !== null && !Array.isArray(data) ? (data['output'] ?? null) : null;
const kept = bytesOf(JSON.stringify(output)) <= 8192 ? output : null;
return [...view, { at: event.time ?? '', run: event.source, output: kept }].slice(-50);
}- The filter writes out the type and the subject:
reasoning/post-announcementnames the runs of that reasoning function. - An output larger than 8 KiB (8,192 bytes) as JSON, or too large for its event, is kept as
null.get_runof the run that an entry names reads the whole output. - An entry then takes at most 8,286 bytes: the output, the time (24), the run (42) and 28 bytes of field names and punctuation. So the view takes at most 50 × 8,286 + 51 = 414,351 bytes, about 405 KiB, under the 512 KiB a view may take.
- Every run of
post-announcementis in the playbook's history, whoever started it, so this view misses none. A log that workflows write into would miss each run that does not write to it.
Fields
| Field | Purpose |
|---|---|
description | Optional; 1 to 1,000 characters |
language | Required; typescript, the playbook's one language |
source.events | Required; 1 to 8 filters, each naming events the fold takes; see The events |
view.initial | Optional; the view before any event, any JSON value within the bound on a view; null when left out |
view.schema | Optional; a JSON Schema the view must match after every fold; initial must match it too |
input.schema | Optional; a JSON Schema of a run's input; a run given no input runs with {} |
output.schema | Optional; a JSON Schema of a run's output; an output that does not match it ends the run as a conflict |
- Unknown fields are rejected. Among them are the fields of a reasoning function that do not apply here:
model,config,tools,output.formatandinput.default. - The body after the front matter is the module. A document without one is rejected.
- A module that exports a function
answercomputes a run's output with it. One that does not returns the view itself. - The saved function has the
media_typetext/markdown. Itsdescription,input_schemaandoutput_schemacome from the document.
The events
A recall function folds the events of its own playbook, in the order the playbook recorded them, from the first. These events are not the whole of the playbook's history:
| What | The events a fold sees | The playbook's history |
|---|---|---|
| Who reads them | A recall function's fold, and also a workflow's event triggers and listen filters and a prediction's dataset | A person or an agent, with list_playbook_events and get_event |
| What they hold | The facts in the table below, and the events published to the playbook or emitted by its workflows | Everything recorded: those facts and events, each run's input on its run_started, tool calls, workflow steps, requests, answers, deliveries |
| The shape of each event | A CloudEvent, with the attributes below | id, type, summary, data and metadata |
So run_started is in both, with the run's input in the history and with no data for a fold. A tool call, a step or an answer is in the history alone, and a fold never sees it. The events a fold sees:
| Events | source | subject | data |
|---|---|---|---|
run_started, run_failed | /runs/<run id> | <type>/<name> | none |
run_succeeded | /runs/<run id> | <type>/<name> | output; or output_bytes, its size, when the output would make the event larger than 240 KiB |
run_rejected | /runs/<run id> | <type>/<name> | reason, and kind where the rejection has one |
definition_created, definition_updated, definition_retired | /definitions/<type>/<name> | none | none |
| An event published to the playbook, of any other type | as its publisher gave it | as given | as given; see Publishing events |
Each event is a CloudEvent with its id, type, source, time and data. Its time is when it happened, as the playbook recorded it. An event the playbook recorded as the effect of another names that one in causationid, and the run at the top of its chain in correlationid. The playbook's own events also carry extension attributes that say how they came about, each where the playbook has it:
| Attribute | Holds |
|---|---|
caller | Who acted: a caller's id; or playbook: and the playbook's id, for a run a trigger started and the runs it calls |
definitionversion | The version of the definition the run runs or the event is about, a whole number from 1 |
depth | On a run a reaction started, how many reactions are above it, a whole number from 1 |
calldepth | On a run a workflow's call started, how many calls are above it, a whole number from 1 |
calledby | The id of the run whose call started the run |
triggerkind | The kind of the trigger that started the run: event, cron or every |
triggerreference | Where that trigger is in its workflow's document |
- No published event carries one of these attributes, nor
causationidorcorrelationid, sincepublish_eventrefuses them. - A recall function never folds the events of its own runs.
- An event the runtime cannot read, such as one whose data nests deeper than 512 levels, is passed over and reported to the operator once.
Filters
A filter names the events it takes. Every value except data is written out, never computed:
| Key | What an event must have |
|---|---|
type | Required; exactly this type |
source | Optional; exactly this source |
subject | Optional; exactly this subject, such as reasoning/review-brief for the runs of one reasoning function |
| Any other attribute | Optional; exactly this value, such as triggerkind: event or caller: playbook:finance: a whole number from 1 for depth, calldepth and definitionversion, and text for any other |
data | Optional; data equal to this value, or data for which an expression such as '${ $data.revenue > 100 }' holds; the expression reads the event's data as $data and nothing else |
- An event is folded when it matches any of the filters, and once only.
- A filter that names an attribute the event does not have does not match it. A filter for runs that no reaction or no call started therefore leaves
depthorcalldepthout. - A filter that names
depth,calldepthordefinitionversionas 0 is refused when the document is saved. - An expression holds unless it returns
falseornull. - An attribute is named as CloudEvents names it, in at most 20 lowercase letters and digits. Any other key is rejected, and so is a computed attribute.
- A
dataexpression that names anything but$datais refused when the document is saved, with the compiler'sCannot find name.
The module
The body is one TypeScript module, as a computation function's program is. It may export two functions:
| Export | What it does |
|---|---|
fold(view: View, event: Event): View | Required; takes the view and one event and returns the next view |
answer(view: View, input: Input): Output | Optional; takes the view and the run's input and returns the run's output; without it, a run returns the view |
The runtime declares these types for the module:
View,InputandOutputareview.schema,input.schemaandoutput.schemaas TypeScript types. Without a schema, each isJson, any JSON value. The types follow the rules of a computation function's types:- An object schema with
propertiesis a closed type of them, therequiredones required, unlessadditionalPropertiesopens it. One withoutpropertiesis an object of any JSON values. enum,const,anyOfandoneOfgive unions. An array gives itsitemsas the type of its elements.- A definition under
$defsordefinitionsis a type named after its schema and the definition, such asInput_address. - What a type cannot say, such as
patternorminimum, is checked when the run checks its input and output.
- An object schema with
Eventis the playbook's event. It has the CloudEvents attributesspecversion,id,type,source,subject,time,datacontenttypeanddataschema, anddataasJson. It also hascausationid,correlationidand the playbook's own extensions:caller,definitionversion,depth,calldepth,calledby,triggerkindandtriggerreference. Each is optional exceptid,typeandsource.
A fold reads the definition of a run's event from event.subject, and who acted from event.caller. Event names no other extension. Read another through a type that names it, such as (event as Event & { region?: string }).region, so that a misspelt attribute is refused rather than read as an extension.
The module may declare types of its own and exported functions, and nothing else at the top level. A module that kept state between two folds would make a view depend on where a page of events ends. Write a helper inside the function that uses it, or export it.
The document is checked when it is saved, as a computation function's is: with the TypeScript compiler in strict mode with exactOptionalPropertyTypes, against these types and the sandbox's own library. Each problem is refused with its line and the compiler's words. The check refuses:
- a module without
fold, or afoldoranswerof another signature, with the signature it needs; - a top-level statement other than a type or an exported function, such as
let, a class or a function the module does not export; import,require,eval,new Function,import(),import.metaand declared names such asdeclare global;async,awaitand an exported generator, since a fold returns at once;- an
enum, anamespace, a parameter property or a decorator; - more than 500 assignments to variables in one function;
- a name the sandbox lacks, as listed below.
The module runs in the QuickJS sandbox that computation functions run in, with the same library and numbers:
- The library is the language's own, ES2023 and more, such as
toSorted,Map.groupBy,Object.groupBy,Set.prototype.union, the iterator helpers andBigInt. - Absent:
Math.random,eval,Function,WeakRef,FinalizationRegistry,Promise,console,fetch, timers,Intl, modules and every local-time method ofDate. new Date(number),new Date(date), a date alone such asnew Date("2026-01-01"), and a date and time with its offset work.- These raise: a
Datefrom several numbers, a date and time without an offset,Datecalled withoutnew,String(date)andtoLocaleString(). UseDate.UTC, thegetUTC…methods andtoISOString(). - Strings compare and sort by UTF-16 code unit, and
localeComparereads no locale. An object keeps its keys in the order they were written. - Numbers are IEEE 754 doubles. Integers are exact up to 2^53, and
0.1 + 0.2is not0.3. There is no decimal type, so keep amounts in whole minor units, such as cents. End a division withMath.floor,Math.ceilorMath.round. - A view and an output are JSON. A
Date, aMap, aSet, a class instance, a function, aBigInt, a symbol,NaN,Infinity,undefinedand a cycle are not. A fold that returns one stalls its view. An answer that returns one ends its run.
A view does not depend on where a page of events begins or ends:
- In a fold,
Date.now()andnew Date()return the time of the event being folded, so a fold gives the same view on every server. In an answer, they return the moment the run started. - The view stays inside the fold's sandbox for a whole page of events. Each fold is given the view the fold before it returned, as JSON gives it back. What JSON does not keep never reaches the next fold: an object two members share, a negative zero, or a member keyed by a symbol.
- The sandbox is frozen once the module is loaded, the prototypes of iterators and generators among it. So nothing a fold writes outside its view reaches the next fold, whether to
Math, to a prototype or to the module.
How the view is kept
The server that runs the playbook's workflows keeps the views:
- It is woken by every event the playbook records through it. A view follows its playbook by the time it takes to fold a page, and by up to a pass (a second) for an event another server recorded.
- It reads the playbook's history in pages of up to 1,000 records. It folds each page in a worker apart from the server, kept ready between pages.
- It writes each view once a page, so a restart folds at most one page again, to the same view.
Saving a recall function, or a new version of one, builds its view from the start of the playbook's history:
- Until the view has caught up, a run is refused
unavailablewith the kindrebuilding. The refusal says how many events the view has folded and how far it is behind, and carriesRetry-After. - The previous version does not serve runs meanwhile, since a run records the latest version.
- A playbook builds at most four views at once, the setting
RECALL_MAX_REBUILDS. The others wait in the order they were saved, and their runs say so. - A newer version replaces an older one that is being built, waiting or stalled.
What a run read
A run never waits for the view: it reads what the playbook has folded so far. Its record says where that is, so a caller who recorded an event a moment ago and does not see it can tell why:
| Record field | What it says |
|---|---|
view.version | The version of the recall function the view is of |
view.checkpoint | Where in the playbook's history the view has read to, an opaque cursor |
view.checkpoint_at | When the playbook recorded the last record the view read |
view.last_event | The id and time of the last event the view folded, null before the first |
view.folded | How many events the view has folded |
The record also holds language, work (the checkpoints the answer spent), duration_ms, input_bytes and output_bytes. get_run returns it with the output.
The standing of a view
get_definition of a recall function adds its view's standing:
| Field | What it says |
|---|---|
state | live once caught up, rebuilding while it is built, waiting for its turn to be built, or stalled |
version | The version the view is of |
checkpoint, checkpoint_at, last_event, folded | As in a run's record |
lag_ms | How far the view is behind: the time of the playbook's newest record less checkpoint_at, to the second on SQLite |
newest_record_at | When the playbook last recorded anything |
stalled | For a stalled view: the event it stopped at, with its id, type and time; the kind of stall; the fold's own error; and its line in the document |
When a view stalls
A fold that cannot take an event stops its view at that event. The view keeps what it folded before, its other events wait, and the playbook's other views go on. The standing names the kind of stall:
| Kind | At that event |
|---|---|
raised | The fold raised an error, a stack overflow among them |
work | The fold did more than its work |
memory | The fold used more memory than its sandbox |
unfit | The fold returned what JSON cannot carry, or a value deeper than 512 levels |
size | The fold returned a view larger than 512 KiB |
schema | The fold returned a view that view.schema refuses |
refused | The module does not load |
time | The fold ran out of its 10 seconds twenty times |
crash | The worker folding the page crashed at this event twenty times |
- The view is checked after every fold, so it stalls at the event that broke it, however the playbook's history falls into pages.
- A fold that runs longer than 10 seconds is tried again on later passes, since its time depends on the machine, without holding back the playbook's other views. It stalls once it has run out of time twenty times.
- Each fold has its own 10 seconds. The folds of the playbook's other views on the same event never count against it, and the view keeps what it folded before that event.
- A filter's
dataexpression runs under a budget of its own, as large as the fold's, and shares the fold's 10 seconds. One that does more than its work stalls the view. One that raises does not take the event. One that runs out of time is tried again, as a slow fold is. - A filter is bounded beside its fold, so one that goes past its bounds stalls its view as a fold does, and a new version is the remedy. The 3 stops in a row and the 200 milliseconds of a workflow's filter do not apply here.
While its view is stalled, a run is refused conflict with the kind stalled. Its detail names the type and time of the event and what went wrong, with the line of the fold, in fixed words. It never gives the fold's own message or the event's values; the standing shows those. The history is the source of the view, so the repair is a corrected version. Saving it builds the view again from the start.
How a run ends
A run checks the input against input.schema, reads the view, and calls answer when the module exports it. It then checks that the answer is JSON and matches output.schema. It ends in one of these ways:
| Ending | When |
|---|---|
succeeded | The answer, or the view, is JSON that matches the output schema |
invalid_input | The input does not match the input schema, with a sentence and a pointer for each problem, or nests deeper than 512 levels |
unavailable, kind rebuilding | The view of the latest version is not built yet, or waits to be built; try again later |
conflict, kind stalled | The view stopped at an event its fold could not take; save a corrected version |
conflict, kind unworkable | The answer could not give an output, as listed below |
unavailable | The answer took longer than a run may, or found no turn to run within its time |
failed | The runtime itself broke down |
A conflict of the kind unworkable means that the answer:
- raised an error, a stack overflow among them;
- did more work or used more memory than a run may;
- returned what JSON cannot carry, or a value deeper than 512 levels;
- returned what the output schema refuses, or what does not fit the run's record.
It names the answer's own error and its line in the document, such as The answer raised an error on line 53: Error: no such campaign. The same view and input give the same result, so running it again does not help.
Bounds
| Bound | Value | When it is reached |
|---|---|---|
| Document | 65,536 bytes in UTF-8, as every definition | Refused when saved |
| Assignments | 500 to variables in one function, as for a computation function | Refused when saved, invalid_input |
| The check at save | 2 seconds of the server's wall-clock time on the check's own worker, counted once it is ready, as for a computation function | invalid_input: simplify the types, split the module, or save again when the server is less busy; unavailable with no checker ready |
| Filters | 8 | Refused when saved |
| Events a view folds | The playbook's whole history, in the order it was recorded, selected by the filters | Not bounded |
| Work of one fold | 500 checkpoints, what one workflow input may do | The view stalls at that event |
| Memory of one fold | 64 MiB, the maximum of the view's sandbox for a page of events | The view stalls at that event |
| Duration of one fold | 10 seconds; the one stall that depends on the machine's speed | Tried again on twenty passes, then the view stalls |
| View | 512 KiB as JSON, checked after every fold, so that a view returned whole fits a run's record | The view stalls at that event |
| Depth of a value | 512 levels, in a view and in an answer; the stack 1 MiB, as for a computation function | The view stalls; an answer ends conflict, unworkable |
| Work of one answer | 500 checkpoints | conflict, unworkable |
| Output | 1,046,528 bytes as JSON: 1 MiB less 2,048 bytes kept for the run's record | conflict, unworkable |
| Duration and memory of a run | 10 seconds and 256 MiB, as for a computation function | unavailable; conflict, unworkable |
| Recall functions a playbook keeps | 32, the setting RECALL_MAX_FUNCTIONS; lowering it below what a playbook keeps refuses saves and nothing else | Refused when saved, conflict |
| Views built at once | 4 a playbook, the setting RECALL_MAX_REBUILDS; a stalled view counts for none | The save succeeds; the standing says waiting |
| Folding time of a page | 2 seconds from its first fold, checked before each fold | The page ends before its next fold; the rest waits for the next page |
| Pages a pass | 10 a playbook | The playbook goes on at the next pass |
| Playbooks followed at once | 4, the setting RECALL_PLAYBOOKS_AT_ONCE | The others wait for their turn |
| Lag | Reported in the standing and the record | Never bounded |
A run's output and its record share 1 MiB, as for every run; see Runs and results. A recall function's record also says where its view had read to, so it keeps more room than most. The configuration reference describes the settings.
Work is counted in checkpoints, as for a computation function. The engine counts one every 10,000 of its steps, where a call or the jump of a loop is a step. So the same fold spends the same checkpoints on every server and at every size of page.
As a measure, the example's fold built a view of 100,000 matching events, over runs of 100 campaigns, in 42.7 seconds: about 0.43 ms a fold. It read pages of up to 1,000 events in a worker kept between pages. Much of that time went to writing the view out after each fold, which grows with the size of the view. Each fold spent no checkpoint. 3,000 of the events gave the same view and the same checkpoints in pages of 7 and of 1,000. Measured on 2026-10-10 on Node 26.10.0 on an Apple M4 Max.
In a workflow
A workflow calls a recall function as it calls any function, with call: run_definition and type: recall; see Calling a function. The task's output is the run's output. It typically goes to a reasoning function whose prompt reads it as any input. A recall function reaches nothing outside the playbook, so a workflow may run it again freely.
- A view follows its playbook by up to a pass and a page's folds. A workflow that recalls what it recorded a moment before may not see it yet. A step sees only the output, not the checkpoint.
- A run rejected with
conflictof the kindstalledorunworkableraises aruntimeerror with status 409 and thatkind. Running it again gives the same result, so a retry policy should not match it. - A view still being built raises the problem type
https://on.auto/problems/rebuildingwith status 503, which a retry may resolve.
This workflow recalls the verdicts the playbook reached on a campaign before. A reasoning function advises on the campaign with those verdicts in front of it, and a computation function counts them. The recall function campaign-reviews is the document at the top of this page. The reasoning function advise-on-campaign:
---
description: Advises whether to approve a campaign, given the verdicts the playbook reached on it before
model: anthropic/claude-sonnet-4-5
input:
schema:
type: object
properties:
reviews: { type: array }
required: [reviews]
output:
format: json
schema: { type: object, properties: { approve: { type: boolean } }, required: [approve], additionalProperties: false }
---
Given the verdicts this playbook reached on the campaign before, {{ input.reviews | json }}, should it approve the campaign?The computation function tally-verdicts:
---
description: Counts the approvals and rejections among the reviews, beside the advice
language: typescript
input:
schema:
type: object
required: [reviews, advice]
properties:
reviews: { type: array, items: { type: object, required: [verdict], properties: { verdict: { type: string } } } }
advice: { type: object, required: [approve], properties: { approve: { type: boolean } } }
---
export default function (input: Input): Json {
return {
approvals: input.reviews.filter((review) => review.verdict === 'approve').length,
rejections: input.reviews.filter((review) => review.verdict.startsWith('reject')).length,
approve: input.advice.approve,
};
}The workflow decide-on-campaign:
document:
dsl: '1.0.3'
namespace: campaign-reviews
name: decide-on-campaign
version: '1.0.0'
summary: Recalls the verdicts on a campaign, asks for advice with them, and counts them.
input:
schema:
document:
type: object
properties:
campaign: { type: string }
required: [campaign]
do:
- recall:
try:
- remember:
call: run_definition
with:
type: recall
name: campaign-reviews
input: { campaign: '${ $data.campaign }', last: 20 }
catch:
errors:
with: { status: 503 }
retry:
delay: { seconds: 5 }
limit:
attempt: { count: 3 }
do:
- give-up:
raise:
error:
type: https://example.com/errors/reviews-unavailable
status: 503
title: The campaign's reviews could not be recalled
detail: The recall function was still unavailable after three retries
export:
as: '${ ({ reviews: $data }) }'
- advise:
call: run_definition
with:
type: reasoning
name: advise-on-campaign
input: { reviews: '${ $context.reviews }' }
- tally:
call: run_definition
with:
type: computation
name: tally-verdicts
input: { reviews: '${ $context.reviews }', advice: '${ $data }' }run_definition of decide-on-campaign takes an input such as {"campaign": "spring-sale"}. While the view is still being built, the recall task retries up to three times, 5 seconds apart. Then give-up raises an error that names what failed, and the run ends rejected as unavailable; Errors, retries and timeouts explains this pattern. A view that has stalled ends the run rejected at once, and its history shows the recall function's run with the stall.
The overview
get_definition returns a saved function with its overview: the function in plain words, for a person who does not read TypeScript or YAML. Auto Studio shows it before the document. The overview says, in order:
- The
description, or that the function has none yet. - What the function remembers: each filter as a sentence, such as "each time a run of the reasoning function review-brief succeeds" for the example at the top of this page. A filter without a
subjectnames every run, and one for a definition's event without asourcenames every definition. Each other attribute and thedataa filter names are said with it, an expression as it is written. - What it keeps: the view's schema in words, with its bounds, such as
maxPropertiesandmaxItems, andview.initialunless it is empty. - What it answers: each field of
input.schemaandoutput.schema, or the view itself for a module withoutanswer. - How the view is kept: a diagram from each filter to the view and from the view to the answer, with one line that the program is TypeScript and is shown under Source. The module itself is not described.
- Used by: the active definitions of the playbook that use this function, read each time it is returned, and left out when there are none.
The overview is written from the document when it is saved, so it says only what the document says. Write the description for the person who will read it, and give each field of the schemas a description, so the overview can say it. A function without them still has an overview, with the types alone.
Availability
Recall functions are available in every runtime. See Functions and availability.