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 withagent().
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.”
One script, top to bottom: the header, then the work in three phases, then one answer back.
ticket-themes.js
- The
metaheader. It names the job and says what it’s for. You see it on the run card before any helper starts. - The phases. Read, Check and Report become the groups on the run card.
parallel(). Eight Read helpers, one per 25 tickets, and the script waits for all of them.agent(). Each call is one helper with its own instructions. Aschemamakes it hand back data instead of prose.log(). A line on the run card, here “9 themes in 8 of 8 batches”.budget. The script checks how much it can still spend, and says so if it has to cut.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.return. The final helper’s answer goes back to your coworker, which turns it into one reply for you.
await and return work at the top level.
The meta header
Every script begins withexport const meta = { … }. The header is read before anything runs, so the plan reaches your run card first.
{ } objects. Variables, function calls and spreads aren’t allowed, because the header is read before the script runs.
name or description, or an integrations value that isn’t a list of names.
Building blocks
The script has seven building blocks, plusworkflow() 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.
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.
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
schemaends without returning the object. - The
promptis empty, or therole,modelorschemaisn’t valid. This throws before the helper starts. - The
agentNamedoesn’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.
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 thenulls from failed jobs. - Pass functions, not calls. Write
() => agent(…). Anything else comes back asnullat 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.
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.
- 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.
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.
- From the slash menu. Typing
/ticket-themes march-tickets.csvsends “Run the ticket-themes dynamic workflow, with this as its input: march-tickets.csv”. Your coworker passes that input on asargs, for example{ file: 'march-tickets.csv' }. It sees only a saved script’s name,descriptionandwhenToUse, 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
argsthe request calls for. - From
workflow(name, args). A script that runs a saved one as a step passes it its ownargs.
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 failedagent(). - Apps come from the top. Its own
integrationsaren’t asked about. Declare every app the run needs in the top-level header, the wayquarter-comparedeclares Slack for the ticket-themes report.
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, underruns/, 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.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 andlog() 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()andMath.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
coderhelper a different path to write to.
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()reads0, and the nextagent()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.
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-3and so on. It never overwrites a file.
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.
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
.jsfiles directly inside.workflows/count. - The slash menu lists every
.jsfile 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:workflow() isn’t asked about separately. Its helpers can use only the apps approved for the top-level run.
Common questions
Do I have to write these scripts myself?
Do I have to write these scripts myself?
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.
Why does a script fail on new Date() or Math.random()?
Why does a script fail on new Date() or Math.random()?
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.What happens when one helper fails?
What happens when one helper fails?
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.Can a run spend more than 2,000,000 tokens?
Can a run spend more than 2,000,000 tokens?
Yes. Ask your coworker for a bigger budget, up to 8,000,000. The limit applies to each stretch of a long run.
Can I edit a saved dynamic workflow?
Can I edit a saved dynamic workflow?
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.
