Skip to content

Slots and Skills

Stability: stable.

A slot is a name the agent uses to reach something without knowing where it is.

Slots are environment variables. The runtime exports them into the agent’s environment and into every process the agent starts, and the shell expands them the way it expands any other variable. The agent writes $OUTPUT, lists $SKILLS, leaves working files in $TMP, and never types a path.

Inside a run, the slots are the runtime’s. The caller’s environment passes through — an agent, hook, or gate finds PATH the way any child process would — except provider credential environment names consumed by the parent; BOT_HOME, the runtime’s own bot-named variable; $BOT_RUN_ID, which every process the run starts receives with the run’s name (the runtime); and TMPDIR, set to the stage’s own scratch ($TMP). Whatever it held under these names is overwritten for every process the run starts, so a slot always means what this specification says it means (invariant 43).

Slot What it is Lifetime
$PWD the caller-owned tree where this stage works the stage
$INPUT a directory holding the stage’s input the stage
$OUTPUT the one file the stage’s output is written to the stage
$TMP an empty scratch directory the stage
$SKILLS every skill the stage can see, flattened the stage
$SUBFLOWS one folder per subflow call the stage has made the stage

$PWD is caller-owned and shared by default. Several runs, and several branches of one run, can be working in the same tree at the same time. A stage may explicitly select a subdirectory instead (the stage). $INPUT, $OUTPUT, and $TMP belong to the runtime, one set per stage, and no two stages share them.

$PWD is the one slot the runtime does not invent — every process has a working directory — but it is a slot because the caller decides what it is.

Without being told otherwise, the agent works in the directory the runtime was started from. Naming one is how a caller separates them, and it is what two concurrent runs need: two runs pointed at two worktrees share no ground, and two runs left at the default share everything.

bot run start --in selects the root workspace. A stage with workdir: ./investigator uses the investigator/ directory beneath that root as its process working directory and as its $PWD. The same effective directory is used by the agent, its local context, its hooks, and its gates. A stage without the field keeps the current flow’s inherited $PWD; this preserves the ordinary one-directory flow and a subflow’s inheritance from its caller.

An authored working directory must exist before the run and remains after it. The runtime neither creates nor cleans it. This is workspace placement, not a sandbox: the operating system still decides what an agent can reach (invariants 34–37).

Where the runtime was started and where the agent works are two different things. The caller names the second one, and the agent’s process is put there: it is the directory the agent starts in, the one relative paths resolve against, and the only tree it has any reason to touch. A runtime run from anywhere can point an agent at a worktree, a checkout, or a scratch clone, and nothing in the assembly changes.

By default $PWD is a path and nothing more: what the tree itself carries — an AGENTS.md, skills of its own — reaches the agent only when the caller asks for it (invocation).

A stage’s input is a directory holding one file per source. A sequential stage has one source, so its $INPUT holds one file. A stage after a fan-out has several, so its $INPUT holds several.

Nothing is ever merged. Three branches produce three files with three names, and the filesystem is the namespace that keeps them apart. This is why the format never has to answer “what does merging two YAML documents mean” — it doesn’t merge them. The rule holds for JSON, for markdown, for an image, for anything.

A file in $INPUT is named after what produced it: the stage’s identity, which is its folder or file name with the leading number removed. What produced it is always something visible from where the stage stands — the previous stage, a branch, a chosen alternative, a loop — never a stage buried inside one of those, so a container’s internals stay its own (invariant 26).

Where the stage sits What $INPUT holds
after 02-design.md design.txt
first in a flow request.txt
first fresh plain root stage in an applicable resumed run its ordinary source plus bot-resume-prior-failure.json or the first deterministic numeric suffix if that name collides
after a PARALLEL with three branches research/, risk/, cost/ research.json, risk.json, cost.json
after a CHOOSE that ran escalate/ escalate.json
after a LOOP named 03-refine/ refine.json — the last repeat’s output, under the loop’s name
the second pass of a LOOP the loop’s own input, and the previous pass’s output

The extension is the one the producing stage’s schema chose, or .txt when it had none (the schema). The run’s request is named request and keeps the extension of the task file it came from, or .txt when it arrived on the command line, on stdin, or in a task file with no extension (invocation).

Two consequences fall out of this. A stage after a CHOOSE can tell which alternative ran by the name of the file it was given, without being told. And a stage after a PARALLEL addresses each branch by name, so branches can produce entirely different shapes without anything having to reconcile them.

The runtime names the files in the prompt, so the agent does not have to discover them (prompt construction).

A stage writes its output to $OUTPUT and nowhere else. Anything else it wrote is not its output.

The stage’s schema chooses the file’s format, and therefore its extension: .json, .md, or .txt when there is no schema at all (the schema).

$OUTPUT is not under $PWD. It lives in a directory the runtime owns for that one stage, which is what makes it safe for several agents to be working in the same tree at once.

A stage that has made subflow calls finds each one under $SUBFLOWS, numbered in the order the calls were made: an input and an output, each with the extension its content chose (subflows). The slot exists so an agent can compose over children’s answers by path, without ever learning where their runs are recorded. A stage that has made no calls has an empty $SUBFLOWS, and a stage with no subflows in scope is not told the slot exists.

$TMP is an empty directory when the stage starts, and no other stage sees it. Nothing in it is recorded or reaches the next stage. The runtime destroys a stage’s $TMP when that stage settles, including a refusal, fault, or handled signal.

The environment a stage’s processes hold also carries TMPDIR, set to the same directory, so a program that mints its own temporary files — mktemp, a language’s temp-directory call — leaves them in $TMP instead of the machine’s temporary directory. Those files remain available while the stage is live and disappear with $TMP.

One directory, two strings. A hook, a gate, and every other process the runtime starts itself receive the backing path in $TMP and TMPDIR. The agent and every process the agent starts receive a short handle instead, and the agent’s own file tools resolve that handle. The Bot runtime makes the handle a symbolic link in the machine’s shared temporary directory, named bot- followed by a hash of the backing path, so that a Unix socket path built inside $TMP fits the platform’s length limit. It refuses a handle already pointing somewhere else and removes only a handle that still names its own backing path. The two strings resolve to the same directory, and stage_start.slots.tmp holds the backing one (the record).

tmp: flow on a FLOW.md makes every stage in that flow share one scratch directory instead, for the case where stages pass working material along that is not an output (flow). That shared $TMP remains through handoffs and is destroyed when its flow settles. Per stage is the default, because per stage is what surprises nobody. A subflow has its own flow and scratch scope, so its cleanup cannot remove its parent’s shared $TMP.

Only $TMP is removed at normal settlement. The surrounding scratch directory retains the other runtime material, and sealed outputs, records, sessions, $INPUT, $SKILLS, and $SUBFLOWS retain their existing contracts. Scratch is outside the working tree ($PWD), does not live under the run’s directory, and sits in a cache directory the runtime owns, so no slot value discloses where runs live (home). The Bot runtime puts that cache at $XDG_CACHE_HOME/bot/tmp, or under ~/.cache when the variable is unset.

Retained scratch is BEST-EFFORT, because a cache is the one directory on the machine whose contents anything may reclaim: the operating system, a cleaner, a full disk, or a person with rm. Nothing a run is judged by is only there. What the run produced was captured into its record when it was produced, so missing retained scratch costs inspection material and no part of an answer. bot run events names only the directories that are still there.

Scratch is created owner-only at its root, like the home (home), and each run’s entry within it is keyed by the home as well as by the run: two homes on one machine can mint the same run name, and one shared directory would let the runs write over each other.

A provider credential environment name consumed by the parent is removed before anything the run starts sees it. This runtime recognizes exactly these names:

AI_GATEWAY_API_KEY ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN
ANTHROPIC_OAUTH_TOKEN ANT_LING_API_KEY AWS_ACCESS_KEY_ID
AWS_BEARER_TOKEN_BEDROCK AWS_CONTAINER_AUTHORIZATION_TOKEN
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE AWS_CONTAINER_CREDENTIALS_FULL_URI
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI AWS_DEFAULT_REGION AWS_PROFILE
AWS_REGION AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_WEB_IDENTITY_TOKEN_FILE
AZURE_OPENAI_API_KEY AZURE_OPENAI_BASE_URL AZURE_OPENAI_DEPLOYMENT_NAME_MAP
AZURE_OPENAI_RESOURCE_NAME BASETEN_API_KEY CEREBRAS_API_KEY
CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_API_KEY CLOUDFLARE_GATEWAY_ID
COPILOT_GITHUB_TOKEN DEEPSEEK_API_KEY FIREWORKS_API_KEY GCLOUD_PROJECT
GEMINI_API_KEY GOOGLE_APPLICATION_CREDENTIALS GOOGLE_CLOUD_API_KEY
GOOGLE_CLOUD_LOCATION GOOGLE_CLOUD_PROJECT GROQ_API_KEY HF_TOKEN KIMI_API_KEY
KIMI_CODE_OAUTH_HOST KIMI_OAUTH_HOST MINIMAX_API_KEY MINIMAX_CN_API_KEY
MISTRAL_API_KEY MOONSHOT_API_KEY NVIDIA_API_KEY OPENAI_API_KEY
OPENCODE_API_KEY OPENROUTER_API_KEY PI_OAUTH_CALLBACK_HOST
QWEN_TOKEN_PLAN_API_KEY QWEN_TOKEN_PLAN_CN_API_KEY RADIUS_API_KEY
TOGETHER_API_KEY XAI_API_KEY XIAOMI_API_KEY XIAOMI_TOKEN_PLAN_AMS_API_KEY
XIAOMI_TOKEN_PLAN_CN_API_KEY XIAOMI_TOKEN_PLAN_SGP_API_KEY ZAI_API_KEY
ZAI_CODING_CN_API_KEY

The list is the set of names this runtime’s pinned providers read, so it grows when they do. A name outside it is a name the runtime cannot recognize, and it reaches every stage. BOT_HOME is scrubbed for a different reason: it is the runtime’s own variable and never the agent’s to see (the home).

Some of those names carry an input to authentication rather than a secret — an account name, a project, a region, an identifier, or the path to where a credential is kept:

AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE AWS_CONTAINER_CREDENTIALS_FULL_URI
AWS_DEFAULT_REGION AWS_PROFILE AWS_REGION AWS_WEB_IDENTITY_TOKEN_FILE
AZURE_OPENAI_BASE_URL AZURE_OPENAI_DEPLOYMENT_NAME_MAP
AZURE_OPENAI_RESOURCE_NAME CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_GATEWAY_ID
GCLOUD_PROJECT GOOGLE_APPLICATION_CREDENTIALS GOOGLE_CLOUD_LOCATION
GOOGLE_CLOUD_PROJECT KIMI_CODE_OAUTH_HOST KIMI_OAUTH_HOST
PI_OAUTH_CALLBACK_HOST

Every name in this second list is scrubbed exactly like the rest. The distinction is for reporting: a provider’s own error report has the value of a secret-bearing name replaced and the value of one of these left as the provider wrote it, because an account name or a region is not a secret and redacting it would claim one (the record).

A test pins this registry to Pi’s own provider sources at the pinned version.

An assembly may declare slots of its own, for the asset every flow in it works against — a knowledge base, a corpus, a reference tree:

slots:
kb: the knowledge base this assembly answers from

The key is the slot’s name; the value is its description, which is what the agent is told about it (prompt construction). The caller supplies each one at invocation as a path (invocation), and every stage of the run finds it exported like any slot: $KB, uppercased.

A declared slot is a place the caller names, where $PWD is the place the work happens. The assembly says what it needs and the home or the caller says where that is — the same portability the model rungs give, applied to paths.

A declared name is an ASCII POSIX identifier: ^[A-Za-z_][A-Za-z0-9_]*$. Its uppercase export must be unique among declared slots, so notes and NOTES are refused together (slot-reserved).

A declared name may not shadow the runtime’s own: input, output, tmp, skills, subflows, pwd are refused (slot-reserved). Nor may it shadow the environment it lands in: slots overwrite the environment (invariant 43), so a declared name whose uppercase is a variable already set at invocation — path becoming $PATH — is refused the same way (slot-reserved), because a slot that overwrote PATH would break every script beneath it. A run that does not supply a declared slot is refused (slot-missing), and a supplied path that does not exist is refused the way --in’s is (path-missing) — a slot points at something real or the run does not start.

The agent is never told where the assembly lives, where the run record lives, or where anything checking it lives. The slot is the whole of what it knows.

Names direct the agent’s attention toward its task and away from the machinery around it. They provide no security boundary.

Bot is not a sandbox. An agent holding a shell can print a slot value and follow any path the operating system permits. Bot retains direct tool calls reported by the model harness. It does not watch the filesystem or claim a complete list of changes. Use an operating-system boundary or container when work needs containment.

Stability: stable.

A skill is a capability the agent can reach for. A flow is a procedure — do this, then this, then this. A skill is the opposite shape: it is horizontal, available at any point, and describes how to do a kind of thing rather than what to do next.

Skills exist so an agent can be given a large capability without carrying all of it in context at once. It sees that a skill exists and what it is for; it reads the rest only when it decides the skill applies.

That only works if the agent is shown as little as possible. A skill belongs at the narrowest scope where it is useful. A skill that only one stage needs is pushed down to that stage, so no other stage ever sees it. A skill every stage needs sits at the top.

A skills/ directory may sit at five places, and its position decides who can see it.

Where Visible to
$PWD that stage, and only under local-context: use
the assembly root every flow and every stage
a flow folder the stages of that flow
a container folder the stages beneath that container
a stage folder that stage alone

Four of those are the assembly’s own. The fifth is not: it belongs to that stage’s effective workspace, and it is admitted only when the caller says so.

A container carries skills the way it carries options: what sits on a LOOP, CHOOSE or PARALLEL folder belongs to everything beneath it — every repeat of a loop, every branch of a parallel, whichever alternative a choice took. It belongs to the stages beneath the container, so the agent making a choice does not read the choice folder’s own skills; the stages it chooses between do. A skills/ folder is a scope, never a branch and never a step in the sequence.

The agent does not see any of that structure. Skills are flattened into one list and reached through a slot:

$SKILLS/<skill-name>/

A stage with skills apple and banana of its own, in a flow with cantaloupe, in an assembly with diamond and elephant, sees exactly five skills — at $SKILLS/apple, $SKILLS/banana, $SKILLS/cantaloupe, $SKILLS/diamond, and $SKILLS/elephant. Nothing in what it sees says which scope any of them came from.

Names collide by overriding, narrowest first. The order is $PWD’s own skills when the run admits them, then assembly, flow, the containers a stage sits inside from outermost to innermost, then the stage: a stage skill named apple replaces a container’s apple, which replaces a flow’s, which replaces an assembly’s, which replaces the workspace’s. One name is one skill.

The workspace is the one scope that is widest without being furthest away. It sits below the assembly rather than above it because the axis here is trust, not proximity: the assembly’s author chose what the agent is for, and the repository being worked on did not. An assembly skill named house-style is the one the agent gets, however near the workspace’s own house-style sits.

A skill follows the Agent Skills layout — the standard at agentskills.io, repository agentskills/agentskills: a SKILL.md describing the capability, and optionally its own scripts/ and reference material alongside. Its frontmatter carries a required description — the one line the agent is shown beside the skill’s name (prompt construction); a SKILL.md without one is refused (key-missing). Unlike a sentinel’s, this frontmatter is otherwise open: Agent Skills carry keys of their own, and the format reads description and ignores the rest.

The folder is the skill’s name, and that is where this format departs from the standard: upstream requires a name key beside description, and here name is an extra key like any other — never read, never refused. Folder as name is what makes narrowest-first override work. This format is stricter in the other direction: each skill sits in its own folder, and no skill tree holds an ASSEMBLY.md at any depth (entry-unknown for either).

A skill’s scripts are reached under its own slot path, so a skill’s contents stay together and a skill never reaches into another skill.

The agent is never told where the assembly lives on disk. The slot is the whole of what it knows: a name, and a path underneath it that resolves (slots). This keeps an agent’s attention on its task, and it removes the map that an agent would otherwise use to go looking for the machinery judging it.