Skip to main content
Every dynamic workflow runs from a short script your coworker writes, and this page shows you how to read one, check it, and change a saved one.

What a script is

When a job is too big for one pass, your coworker writes a short JavaScript script for it. The script says what each helper does, which helpers run side by side, and how the results come together. Your coworker writes a new script for each big job. You don’t have to write one yourself. A script you keep with Make repeatable is saved in the coworker’s own files and reused by name. You can open a saved one, read it, and edit it. The script itself does no work. It only plans. Every piece of real work happens inside a helper, started with agent(). To turn the capability on and see a run from your side, start with Dynamic workflows.

Anatomy of a script

Here is the script Ivy, a support coworker, writes for “Go through all 200 support tickets in tickets.csv and group them by theme. Post the summary to #support-leads.”
The ticket-themes script in a code panel, with numbered callouts on the meta header, the phases, agent(), parallel(), log(), budget, pipeline() and the return line

One script, top to bottom: the header, then the work in three phases, then one answer back.

ticket-themes.js
Read it in the order the numbers on the figure follow:
  1. The meta header. It names the job and says what it’s for. You see it on the run card before any helper starts.
  2. The phases. Read, Check and Report become the groups on the run card.
  3. parallel(). Eight Read helpers, one per 25 tickets, and the script waits for all of them.
  4. agent(). Each call is one helper with its own instructions. A schema makes it hand back data instead of prose.
  5. log(). A line on the run card, here “9 themes in 8 of 8 batches”.
  6. budget. The script checks how much it can still spend, and says so if it has to cut.
  7. pipeline(). Each theme is checked and then written up on its own, so one theme’s write-up can start while another is still being checked.
  8. return. The final helper’s answer goes back to your coworker, which turns it into one reply for you.
Everything between the helper calls is ordinary code. Merging the batches and sorting the themes costs nothing, because no helper is needed for it. The script runs as one async function, so await and return work at the top level.

The meta header

Every script begins with export const meta = { … }. The header is read before anything runs, so the plan reaches your run card first.
The header must be written out with plain values: text, numbers, lists and { } objects. Variables, function calls and spreads aren’t allowed, because the header is read before the script runs.
A header that can’t be read stops the script before anything runs. No helper starts, nothing is spent, and your coworker fixes the header and tries again. The same goes for a missing name or description, or an integrations value that isn’t a list of names.
Use the same titles in meta.phases and in your phase() calls. Then each group on the run card fills in as the script reaches it.

Building blocks

The script has seven building blocks, plus workflow() for running a saved dynamic workflow as one step. Each one is described below with what it returns and what happens when something goes wrong.

agent(prompt, options)

Starts one helper with the instructions in prompt, waits for it to finish, and returns its answer. Without a schema, the answer is the helper’s closing summary as text.
Each helper starts fresh. It can’t see your conversation with your coworker. So its instructions must stand on their own: name the file, the rows, and the answer you want. What each role may do: A helper with a role can’t use your connected apps unless the run’s header declared them and you approved them. No helper can start helpers of its own, start another dynamic workflow, or ask you anything. If a step would need your approval, the helper is refused and reports that instead. A custom helper named with agentName gets exactly the tools its definition lists. If its definition lists none, it gets everything your coworker has in that turn, except starting helpers and asking you. Anything that would ask you first is still refused, unless you approved that app for the run. Getting data back with schema. Give a helper a schema and you get an object you can count, sort and filter, instead of prose.
If the helper’s answer doesn’t match the shape, it’s told what was wrong and tries again. Using a custom helper. Name it, and it brings its own instructions.
When it fails. agent() throws an error, and the script decides what to do. That happens when:
  • The helper fails. The error names the helper and points to its transcript.
  • A helper with a schema ends without returning the object.
  • The prompt is empty, or the role, model or schema isn’t valid. This throws before the helper starts.
  • The agentName doesn’t match a custom helper. That helper fails.
  • The run reaches a limit: 30 helpers, or not enough budget left. See Limits.
  • The run reaches the end of a stretch and pauses. See Long runs and retries.
  • The run is stopped.
A helper’s cost counts even when it fails. A helper you stop from the run card either throws, or, without a schema, can come back as empty text. A bare agent() that throws ends the whole script unless you catch it. Inside parallel() and pipeline(), a failure only affects its own item. Each helper’s full transcript is saved in the task’s files, under subagents/. The transcript link on its row opens it.

parallel(thunks)

Runs a list of jobs side by side and waits for all of them. Each job is a function that starts the work, usually () => agent(…). It returns a list of answers in the same order as the jobs, whatever order they finished in. A job that throws comes back as null, and the others carry on.
  • It waits for everyone. Nothing after parallel() runs until every job has finished or failed. Use it when the next step needs all the results at once, like merging batches.
  • Six helpers work at once. That limit covers the whole run, including nested calls and saved dynamic workflows run as steps. The rest wait their turn, in order.
  • Up to 256 jobs per call. Pass more and the call throws before any of them runs. Split the work across several calls instead.
  • Filter before you use the results. results.filter(Boolean) drops the nulls from failed jobs.
  • Pass functions, not calls. Write () => agent(…). Anything else comes back as null at once, and a helper already started that way still runs and costs.

pipeline(items, ...stages)

Moves each item through a series of stages, on its own. There is no waiting between stages, so one item can be in its last stage while another is still in its first. Each stage is a function. It gets three things: what the stage before returned, the original item, and the item’s position in the list. The first stage gets the item itself.
pipeline() returns what each item’s last stage returned, in the order of the items. If a stage throws, that item comes back as null and its later stages are skipped. The other items carry on. It shares the same limits as parallel(): six helpers at once across the run, and up to 256 items per call. Which one to use. Pipeline is the better default for work with stages. Items don’t wait for each other between stages. Use parallel() only when a step needs every earlier result at once.

phase(title)

Starts a group on the run card. Every agent() after it goes into that group, unless the helper names its own phase.
A phase declared in the header shows as “pending” until the script reaches it. A title the header didn’t declare is added to the card when it’s first used. An empty title is ignored. phase() returns nothing.

log(message)

Adds a line of text to the run card, so you can follow along. Use it to say what the script found, decided or left out. log() takes text. Anything else, and empty text, is ignored. It returns nothing, and the lines aren’t part of the result your coworker gets back. After a pause the script runs again from the top, so its log() lines appear again.

budget

The run’s spending limit, so the script can plan within it. It is counted in tokens, the unit AI models are billed in.
How the limit is set. Your coworker picks it when it starts the run. The default is 2,000,000 tokens, and the most is 8,000,000. If a job needs more, ask your coworker for a bigger budget. How the limit holds. Before each helper starts, the run sets aside room for it:
  • Until any helper has finished, it sets aside 60,000 tokens, or a tenth of the limit if that’s smaller.
  • After that, it sets aside the average cost of the helpers that have finished, and never less than that first amount.
That’s what stops a whole batch from starting when there’s only room for a few. A helper can still cost more than was set aside, so a run can end somewhat over its limit. When there isn’t enough room left, agent() throws “Token budget exhausted” and no more helpers start. Catch it and return what you have, and the run completes with partial results. Or let the run fail, and a retry of the same script with a bigger budget reuses every finished helper. See Long runs and retries.
The limit counts the current stretch of a long run, not the run as a whole. Each stretch starts with the limit your coworker passes when continuing, or 2,000,000 if it passes none.

args

The input the script was started with, or undefined when there is none. This is how one saved dynamic workflow serves many requests.
Input can arrive three ways:
  • From the slash menu. Typing /ticket-themes march-tickets.csv sends “Run the ticket-themes dynamic workflow, with this as its input: march-tickets.csv”. Your coworker passes that input on as args, for example { file: 'march-tickets.csv' }. It sees only a saved script’s name, description and whenToUse, so say there what input it expects, like “args: file, the CSV to read”.
  • From your coworker. It can run a saved dynamic workflow with whatever args the request calls for.
  • From workflow(name, args). A script that runs a saved one as a step passes it its own args.
Keep args small. Over 16 KB of data, it’s dropped without an error, and the script sees undefined. Pass a file name, not the file’s contents, and let a helper read the file. The same script with different args is a different run, so nothing finished in one is reused in the other.

workflow(name, args)

Runs one of your coworker’s saved dynamic workflows as a single step, and returns what it returns. Despite its name, workflow() runs a saved dynamic workflow, not one of your Workflows. name is the saved one’s meta.name. args becomes its args.
  • One run, one card. Its helpers appear on the same run card, under headings that start with ”▸ ticket-themes”, like ”▸ ticket-themes · Read”, with a log line saying it started.
  • Shared limits. It shares the run’s budget, its 30-helper limit, its six-at-once limit and its stop.
  • One level only. A saved dynamic workflow run this way can’t call workflow() itself. Trying throws.
  • Failure throws. If the step fails, or no saved one has that name, workflow() throws. Catch it like a failed agent().
  • Apps come from the top. Its own integrations aren’t asked about. Declare every app the run needs in the top-level header, the way quarter-compare declares Slack for the ticket-themes report.
The saved file is read when the step runs. An edit to it shows up the next time the step runs, even in a run that’s already going.

What the script returns

Whatever the script returns goes back to your coworker as the run’s result. Return plain data: text, numbers, lists and objects. Your coworker reads it, checks it, and turns it into one answer for you. You never have to read the raw result yourself. A result over 8,000 characters is cut short for your coworker. The whole of it is saved in the task’s files, under runs/, and your coworker is told where.

Patterns

Three shapes that make a run’s result worth trusting.

Check the work with a second helper

A helper’s report is its own account of its work. Have a different helper check it, and give each checker its own lens rather than asking two of them the same question.

Loop until a round finds nothing new

For open-ended searching, keep going until a round turns up nothing new, rather than stopping after a fixed number of rounds. Bound the loop by the budget, and by a round count, so it can’t run away.
Keep a helper after a loop like this. At the end of a stretch, budget.remaining() reads 0, so the loop stops without knowing why. The next agent() call is what pauses the run so it can continue. A script that simply returns there finishes early, with what it had.

Cover a bounded set, and say what you dropped

When there’s more to cover than a run can hold, cover the most important part and log() the rest. A silent cut reads as “covered everything”.

Rules every script follows

A script must give the same instructions every time it runs, so a paused or retried run can pick up where it stopped. These rules keep it that way.
  • No clock, no randomness. Date.now(), new Date() and Math.random() throw. A date you pass in as text still works. Vary work by position or by input instead.
  • No outside access. The script has no require, no filesystem and no network. Everything happens through a helper.
  • 64 KB per script. A longer script is refused before anything runs. Put the bulk in files your helpers read.
  • Writers need their own files. Helpers share the task’s files, and nothing takes turns between two writing at once. Give each coder helper a different path to write to.
Each of the first three lines throws on its own:

Limits

Long runs and retries

A big run doesn’t have to fit in one go. It works in stretches of about 10 minutes. At the end of a stretch:
  • No new helper starts. Helpers already working finish, and their answers are kept.
  • budget.remaining() reads 0, and the next agent() throws a pause message instead of starting a helper.
  • The run card shows its paused notice (see Long runs), and its header counts what’s done, like “14 done · continuing”.
  • Catching the pause message and returning doesn’t end the run. It still counts as paused.
Picking up again. The run continues on its own in a new turn, without you asking. Your coworker continues the same run, with the script and args it started with. If a new turn can’t be booked, your coworker is told to continue the run itself, or tell you it’s unfinished. The script then runs again from the top. Every helper that already finished hands back its kept answer at once, at no cost, so the script moves quickly to where it stopped. Retries work the same way. In the same task, run the same script with the same args again, and finished helpers are reused. That applies after a pause, a crash, a failure, a stop, or a spending-limit error the script didn’t catch. Change the script by even one character, or change its args, and it’s a new run that starts from scratch. How a helper is matched. Within a run, each finished helper is matched by what it does: its instructions, model, role, agentName and schema. Its label, phase and the order it finished in don’t matter. A helper whose instructions differ is a new helper. What is never reused:
  • A run that completed. Running the same thing again later starts fresh, so you never get last week’s answers as new.
  • A helper that failed.
  • An answer over 32 KB, which wasn’t kept.

Saving a dynamic workflow

A saved dynamic workflow is a script file in your coworker’s own files, at .workflows/<name>.js. Your coworker can then run it again by name, and so can you, from the slash menu. Make repeatable writes it for you. On a completed run, Make repeatable saves the run’s script. It also adds a workflow to your Workflows that runs it. See Dynamic workflows for what it creates.
  • The file name comes from meta.name: lowercase, with anything other than letters and numbers turned into a dash, up to 60 characters.
  • If the same script is already saved under that name, nothing new is written.
  • If a different script holds the name, the new one is saved beside it as <name>-2.js, then -3 and so on. It never overwrites a file.
Keep meta.name and the file name the same. Your coworker finds a saved dynamic workflow by its meta.name. The slash menu lists it by its file name. When the two match, both find the same script.
If Make repeatable saved a run beside an existing one as ticket-themes-2.js, open the new file and change its meta.name to ticket-themes-2. Until you do, the new copy takes over the original’s name, because two files with the same meta.name can’t both be used. Asking for ticket-themes, or running any workflow that runs it, may run the new copy, and /ticket-themes-2 finds nothing.
How your coworker picks one. Every turn, your coworker sees the list of saved dynamic workflows. It sees each one’s name, its description, and its whenToUse. It’s told to prefer a saved one that fits over writing a new script. A clear whenToUse is what makes that choice work. Reading and editing. Open the coworker’s Files tab and look in the .workflows/ folder. A saved script opens in the editor, so you can read it and change it.
  • An edit applies the next time that dynamic workflow starts.
  • A run that’s already going keeps the script it started with. A saved one it runs as a step with workflow() is read fresh each time.
  • A file whose header can’t be read, or over 64 KB, is skipped. Only .js files directly inside .workflows/ count.
  • The slash menu lists every .js file there by its file name, even one that’s skipped. A saved one only runs while Dynamic workflows and Code and files are on.

Apps inside a run

A helper with a role can’t use your connected apps unless the script asks first. Declare the apps in the header:
You’re asked once, before anything runs. See Letting a run use your apps for the card and what Approve and Decline do. The approval ends with the run: a continuing stretch doesn’t ask again, an app you connect later isn’t added, and a new run asks again. Name matching is loose. “slack”, “Slack” and “Slack Bot” all match a connected app called Slack Bot. The approval card names the apps exactly as they’re connected. A name that matches no connected app stops the run before anything starts. Your “never” rules still win. Approval lets helpers use the app without asking. It never overrides a permission rule that says never. If an approved app is disconnected by the time a long run continues, the run carries on without it. The run card says so. Declare every app in the top-level header. A saved dynamic workflow run as a step with workflow() isn’t asked about separately. Its helpers can use only the apps approved for the top-level run.
Often the better shape is no apps at all. Let the run gather its findings and hand them back, and your coworker sends the result itself, with the usual approvals.

Common questions

No. Your coworker writes one for each big job. You only need this page to read a plan, review a saved one, or make a change to it.
A paused or retried run starts the script again from the top. It can only reuse finished helpers if it gives the same instructions each time. The clock and random numbers would change them, so both are blocked. Pass a date in through args instead.
Inside parallel() or pipeline(), that one answer comes back as null and the rest carry on. A bare agent() throws, and the script can catch it. Its transcript is saved in the task’s files.
Yes. Ask your coworker for a bigger budget, up to 8,000,000. The limit applies to each stretch of a long run.
Yes. Open the coworker’s Files tab, then the .workflows/ folder. Your edit applies the next time it starts.

Next steps

Dynamic workflows

Turn it on and follow a run from your side.

Custom helpers

Give your coworker named specialists a script can call.

Slash commands

Run a saved dynamic workflow by name.

Approvals

What still needs your yes, inside a run and out.