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).
The slots
Section titled “The slots”| 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).
$INPUT is a directory
Section titled “$INPUT is a directory”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.
The names inside $INPUT
Section titled “The names inside $INPUT”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).
$OUTPUT is one file
Section titled “$OUTPUT is one file”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.
$SUBFLOWS
Section titled “$SUBFLOWS”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.
What is scrubbed
Section titled “What is scrubbed”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_TOKENANTHROPIC_OAUTH_TOKEN ANT_LING_API_KEY AWS_ACCESS_KEY_IDAWS_BEARER_TOKEN_BEDROCK AWS_CONTAINER_AUTHORIZATION_TOKENAWS_CONTAINER_AUTHORIZATION_TOKEN_FILE AWS_CONTAINER_CREDENTIALS_FULL_URIAWS_CONTAINER_CREDENTIALS_RELATIVE_URI AWS_DEFAULT_REGION AWS_PROFILEAWS_REGION AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_WEB_IDENTITY_TOKEN_FILEAZURE_OPENAI_API_KEY AZURE_OPENAI_BASE_URL AZURE_OPENAI_DEPLOYMENT_NAME_MAPAZURE_OPENAI_RESOURCE_NAME BASETEN_API_KEY CEREBRAS_API_KEYCLOUDFLARE_ACCOUNT_ID CLOUDFLARE_API_KEY CLOUDFLARE_GATEWAY_IDCOPILOT_GITHUB_TOKEN DEEPSEEK_API_KEY FIREWORKS_API_KEY GCLOUD_PROJECTGEMINI_API_KEY GOOGLE_APPLICATION_CREDENTIALS GOOGLE_CLOUD_API_KEYGOOGLE_CLOUD_LOCATION GOOGLE_CLOUD_PROJECT GROQ_API_KEY HF_TOKEN KIMI_API_KEYKIMI_CODE_OAUTH_HOST KIMI_OAUTH_HOST MINIMAX_API_KEY MINIMAX_CN_API_KEYMISTRAL_API_KEY MOONSHOT_API_KEY NVIDIA_API_KEY OPENAI_API_KEYOPENCODE_API_KEY OPENROUTER_API_KEY PI_OAUTH_CALLBACK_HOSTQWEN_TOKEN_PLAN_API_KEY QWEN_TOKEN_PLAN_CN_API_KEY RADIUS_API_KEYTOGETHER_API_KEY XAI_API_KEY XIAOMI_API_KEY XIAOMI_TOKEN_PLAN_AMS_API_KEYXIAOMI_TOKEN_PLAN_CN_API_KEY XIAOMI_TOKEN_PLAN_SGP_API_KEY ZAI_API_KEYZAI_CODING_CN_API_KEYThe 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_URIAWS_DEFAULT_REGION AWS_PROFILE AWS_REGION AWS_WEB_IDENTITY_TOKEN_FILEAZURE_OPENAI_BASE_URL AZURE_OPENAI_DEPLOYMENT_NAME_MAPAZURE_OPENAI_RESOURCE_NAME CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_GATEWAY_IDGCLOUD_PROJECT GOOGLE_APPLICATION_CREDENTIALS GOOGLE_CLOUD_LOCATIONGOOGLE_CLOUD_PROJECT KIMI_CODE_OAUTH_HOST KIMI_OAUTH_HOSTPI_OAUTH_CALLBACK_HOSTEvery 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.
Declared slots
Section titled “Declared slots”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 fromThe 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.
Why names and not paths
Section titled “Why names and not paths”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.
Skills
Section titled “Skills”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.
Progressive disclosure
Section titled “Progressive disclosure”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.
The scopes
Section titled “The scopes”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.
What the agent sees
Section titled “What the agent sees”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.
What a skill contains
Section titled “What a skill contains”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.
Why the flattening matters
Section titled “Why the flattening matters”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.