Orchestration
Let a space, a run or a scheduled deputy start work of its own and follow it to the end. With the orchestration package, an agent that takes an instruction from Slack launches a run for it, and a loop that polls your ticket tracker launches one run per ticket. No personal token ever enters the container: Marshal holds the credential for the space on the platform side and revokes it when the space or run ends.
What you can do with it
- Ticket loop. A scheduled deputy lists the open tickets, launches one child run per ticket from your
ticket-workermanifest, waits for each, and posts the results. - Slack instruction. An agent in your space reads a request from Slack, starts a child run to do it, and answers in the thread when the run finishes.
- Fan-out review. A run splits a large change into parts, launches a child run for each part, and merges the answers when they come back.
How it works
The parent is the space or run itself. When it starts with the package, Marshal creates a credential bound to that one space or run and keeps it outside the agent’s reach. The agent calls the tools, and Marshal attaches the credential for it. When the space or run ends, the credential is revoked. Nothing the agent can read or copy reaches anything else in Marshal.
Every call passes one permission check, whether it comes from an MCP tool or from a script. A parent can follow and control only its own children. Any other run answers as if it does not exist.
Add it
An admin adds the marshal-orchestration package to a stack and sets its limits there. The stack can include it in every manifest built on it, or offer it for a manifest to add. A manifest can leave an included package out unless the stack pins it, and a manifest cannot add the package on its own: on a stack without it, the manifest shows a violation telling you to add it to the stack. A space or run without the package has no orchestration tools and no credential, so nothing in it can start anything.
The stack sets the defaults. A manifest can only narrow them: fewer tools, lower limits and lower ceilings. A manifest value that goes beyond the stack’s is dropped and the stack’s value applies. The manifest editor shows what the stack allows under each limit and holds your value to it. A stack admin gets a link to raise it on the stack.
The manifests a child may use are the manifest owner’s choice. Out of the box, a child runs on the parent’s own manifest (same_as_mine). The manifest’s owner can name others on its own package entry, and every launch still checks that the person behind the parent may use the child’s manifest. An admin who wants the stack to decide turns off allow_other_manifests: the stack’s manifests list then applies, and a manifest can only narrow it. A space or run keeps the config it started with. A change applies to spaces and runs that start afterwards.
Configure it
The config lives on the package entry in the stack, and a manifest sets its own on its entry. In the stack builder, these are the package’s Integration settings.
tools: [launch_run, launch_deputy, get, list, wait, logs, cancel]
manifests: [ticket-worker, same_as_mine] # usually set on the manifest
deputies: [triage-deputy] # leave empty to allow any deputy the person may run
allow_other_manifests: true
max_concurrent: 3
max_per_hour: 30
max_depth: 1
child:
timeout: { default: 1800, max: 7200 } # seconds
compute_class: { default: small, max: medium } # compute class slugs or ids
permission_mode: { default: ask, max: ask } # deny, ask or auto| Key | What it does | Default |
|---|---|---|
tools | Which tools the parent gets: launch_run, launch_deputy, get, list, wait, logs, cancel are on by default. launch_space, continue, message, get_space, push_space, exec_space and control_space are opt-in. | launch_run, launch_deputy, get, list, wait, logs, cancel |
manifests | The manifests a child may use, by id, slug or name, or same_as_mine for the parent’s own manifest. Set on the manifest, or on the stack as the default. | same_as_mine |
deputies | The deputies a space may fire, by id, slug or name. A manifest can only narrow the stack’s list. | Empty: any deputy the person may run |
allow_other_manifests | Whether a manifest picks its own manifests list. Off, the stack’s list applies and a manifest can only narrow it. A manifest can turn it off for itself, not on. | On |
max_concurrent | Children running at once. | 3 |
max_per_hour | Children started in any rolling hour. | 30 |
max_depth | How many levels of children may nest. A depth of 1 allows children and no grandchildren. | 1, up to 3 |
child.timeout | Default and maximum run time for a child. | The usual run timeout. max is the ceiling you set. |
child.compute_class | Default and maximum compute size for a child. | The usual compute class. max is the ceiling you set. |
child.permission_mode | Default and maximum permission mode for a child. | The maximum is the parent’s own mode: a run in auto launches auto children, a space launches ask children. |
The get tool includes a child’s output.
A launch that asks for more than a ceiling is refused. A launch that asks for nothing gets the default.
Permission modes
A child run started with launch_run never runs above the run that launched it. Children run unattended, so a run in auto starts its children in auto and they do not stop to ask, unless child.permission_mode.default names a lower mode. A space is interactive, so its children start in ask. Set child.permission_mode.max on the stack to change the ceiling: ask or deny keeps children on a shorter leash, and auto lets a space start auto children.
| Parent | child.permission_mode.max | Children can run in |
|---|---|---|
Run in auto | not set | auto, ask, deny |
Run in auto | ask | ask, deny |
Run in ask | not set or auto | ask, deny |
Run in deny | any | deny |
| Space | not set | ask, deny |
| Space | auto | auto, ask, deny |
| Space | ask | ask, deny |
| Space | deny | deny |
A run’s own mode always caps the setting. A run in ask under a stack that sets max: auto starts its children in ask; start the run in auto when its children should run unattended. A manifest can lower max for its own spaces and runs, and cannot raise a value the stack sets.
A change to child.permission_mode applies to runs and spaces started after the change. A launch that goes over the ceiling is refused, and the refusal says whether the ceiling comes from the package config or from the parent run’s own mode.
Fire a deputy from a space
An agent can hand work to the deputies its person already runs. A triage deputy, a release deputy or your team’s review deputy can take a mission from a loop without you leaving the space.
- The agent calls
deputies_listand sees the deputies its person may run, with their missions, the inputs each mission takes, and whether firing it needs approval. - It calls
deputies_launchwith a deputy, a mission and the inputs. - Your own deputies start at once. The agent gets the run id back.
- A team- or org-owned deputy waits for you. Those deputies carry access the space does not have, so the request appears on Home under Needs you, and in your inbox through the MCP, where you approve or deny it. The card shows the deputy, the mission, the inputs and the space or run asking. Approving checks again that you may run the deputy, then starts it.
- The agent calls
deputies_launch_statuswith the request id and gets the outcome: waiting, the run id, denied, expired or refused with the reason.
A request expires when the space or run ends, or after 24 hours. An expired request leaves Home and your inbox.
A fired deputy run is a child like any other. It counts against the package’s limits, and the agent follows it with runs_get, runs_wait and runs_logs and stops it with runs_stop. A mission that is already at its own concurrency limit is refused, so the agent can try again later. Deputy firing is on by default. To switch it off for a stack, leave launch_deputy out of tools. To restrict it, name the deputies in deputies.
Manage child spaces
A parent with launch_space starts long-lived interactive spaces. Four more capabilities let it follow those spaces and work in them, with no CLI and no personal token in its container. Each one is off until the stack or manifest names it in tools.
| Capability | MCP tools | What the parent can do |
|---|---|---|
get_space | spaces_get, spaces_list | Read a child space by id, slug or name, and list its children that are not archived. Poll spaces_get until the state is running. |
push_space | spaces_upload | Put a file of up to 10 MB into a running child’s inbox. The answer is the file’s path in the space. |
exec_space | spaces_exec | Run one command in a running child and get its exit code, stdout and stderr. |
control_space | spaces_stop, spaces_archive | Stop a child, keeping its workspace disk, or archive it. The package config is the approval, so no one is asked to confirm. |
What exec_space hands over. A command from spaces_exec runs as the child’s agent user with everything the child’s manifest binds, its credentials and tools included, and it does not pass through the child’s tool policy or approvals. Allow-list for children only manifests whose access you are happy for the parent to use.
Children only. These tools reach only the spaces this parent launched with spaces_launch, in the same organization, owned by the person the parent acts for. Every other space, including the person’s own spaces that this parent did not start, answers as not found. A name or slug is looked up among the parent’s children only. A deputy owned by a team or the org has no child spaces.
Running a command. spaces_exec takes the command as a list, for example ["mv", "/home/agent/shared/inbox/slack.env", "/workspace/.env"], and runs it as the space’s agent user without a shell. For pipes, redirects or &&, pass ["sh", "-c", "..."]. Each call is bounded:
- It stops after
timeout_sseconds: 60 by default, up to 600. The answer then saystimed_outand carries the output so far. - Each stream keeps up to 256 KiB. A command that keeps writing past that is stopped, and the answer says
truncated. - One command runs at a time in each space. A second call while one runs is refused as busy.
- Ending the call stops the command.
A process meant to keep running, such as an agent you start in the child, should run detached and write its output to a file, for example ["sh", "-c", "nohup ./start.sh > start.log 2>&1 &"].
The output comes from a process in the child space, so the parent receives it as untrusted data, the same way it receives a child run’s output.
Stopping and archiving. Stopping a child that is already stopped, or archiving one that is already archived, leaves it as it is and says so, so a retried call is safe. A child that is still stopping is archived once it has stopped: the call says so, and the parent retries. A stopped or archived child no longer counts against max_concurrent. The person can restore an archived child from the web UI.
A typical flow:
Limits that always hold
Marshal applies platform limits on top of your config:
- Each tree of parents and children has at most 20 running at once and 100 started per hour.
- A child’s descendants count against every ancestor. If your run has a child that has started a grandchild, both count against your run’s limits.
- Child runs started for a person count against that person’s MCP run cap as well.
- Depth is capped at 3, whatever the config says.
When a launch is over a limit, the refusal names the limit and the count, for example the concurrent limit at 3 of 3. A loop can read that, wait for a child to finish, and try again.
Call it from an agent or a script
MCP tools. The agent sees runs_launch, runs_get, runs_list, runs_wait, runs_logs and runs_stop, plus deputies_list, deputies_launch and deputies_launch_status for firing deputies, and spaces_launch, runs_continue, runs_message and the child-space tools when you turn those on. When a child is waiting on an ask, runs_message attaches your message to that ask (attached_to_ask), so the child reads it together with the answer instead of after it. It lists only the tools your config allows. In a space, runs_launch takes a manifest and a task and starts a child on one of the allowed manifests. Pass same_as_mine as the manifest to start the child on the space’s own manifest.
marshal-agent runs. Scripts and loops use the same tools from the shell:
marshal-agent runs launch --manifest M --task T [--title X] [--timeout-s N]
marshal-agent runs get|wait|logs|output|cancel <run-id>
marshal-agent runs list [--status S]
marshal-agent runs continue <run-id> --task T
marshal-agent runs message <run-id> --message MAdd --json to any command for machine-readable output. A command exits 0 on success and 1 when the platform refuses, with the reason on stderr. It exits 3 when the space has no orchestration package and 4 when the platform cannot be reached and a retry is safe.
A loop that starts one run per ticket:
for ticket in $(my-tracker open-tickets); do
# Wait for room: a refusal at the concurrency limit exits 1, so retry.
until id=$(marshal-agent runs launch --manifest ticket-worker \
--task "Fix ticket $ticket" --json | jq -r .run_id); do
sleep 30
done
ids="$ids $id"
done
for id in $ids; do
marshal-agent runs wait "$id" --json > /dev/null
marshal-agent runs output "$id"
doneWho owns the children
A child runs as the same principal as its parent.
- In a person’s space or run, and in a deputy that person owns, children belong to that person. They resolve their own credentials exactly as if the person had launched them, and Marshal checks that the person may use each manifest.
- In a deputy owned by a team or the org, children belong to that team or org. Only org manifests can be used, and child spaces cannot be started.
Nothing is inherited from the parent’s container.
See the children
- The Children panel on the run page, and in a space’s side panel, lists each child with its status, duration, LLM cost and compute cost, a count by state, and the totals.
- Each child links back to its parent with a Started by link.
- The Runs list has a Parent column and a Started by filter that shows only one parent’s children.
- Cancelling a parent that has running children offers to cancel them too. Nothing is cancelled unless you choose it.
- Ending a space does not stop its children. They keep running and still show their parent.
For administrators
The Orchestration section of the admin dashboards shows the parents that started the most children, children started per hour, refusals by limit, and the LLM and compute cost of each parent’s children, over the window you choose. It is how you spot a loop that is running hot.
The organization setting In-space orchestration is on by default. Turn it off and no new credentials are issued, and every orchestration call from a running space is refused on its next call. Changing the setting is recorded in the audit log.
Who sees the audit events
Every orchestration call is in the audit log: each launch, cancel, continue and message, each refusal, and each read such as get, wait and logs. An event names the space or run that acted and the person it acted for.
For child spaces, each command, upload, stop and archive is recorded in the child space’s own audit trail, with the parent that acted and the person it acted for. The parent space’s own audit trail does not list them: find them in the child’s trail, or search the organization’s audit log. A command is recorded when it starts and again when it ends, by its program name, its number of arguments, a short fingerprint of the whole command, and at the end its exit code and how long it ran. If the start cannot be recorded, the command does not run. A command that is refused before it starts is recorded as not started, with no exit code. A program name that contains a credential-shaped fragment is recorded as (redacted). An upload is recorded by its file name and size. The rest of a command, file contents and output never enter the audit log, so a token passed in a command or a file stays out of it.
- Members see the events for their own spaces and runs.
- Admins see the events for the whole organization.
- Events from team-owned or org-owned deputies have no person behind them, so only admins see them.
Try it
Add marshal-orchestration to a stack, allow one manifest for children, start a space from a manifest on that stack, and run:
marshal-agent runs launch --manifest ticket-worker --task "Summarize the open pull requests"