where is the agent's CODE?
You've met agents — a model on a schedule, with files, tools, and memory, hired for outcomes. Now you want to change one. Make it tick every ten minutes instead of every hour. Swap it onto a cheaper model. Give it a colleague that runs alongside it. Forbid it from touching the live page. And your reflex, trained by every other platform you've used, is the same: open the source, find the class, edit the behavior.
So you go looking for the agent's code. The fear underneath the search is that agent behavior is buried in engine internals — that tuning means forking, that the schedule is hard-wired somewhere in a scheduler you don't own. It's a reasonable fear. It's just wrong here, and the way it's wrong is the whole lesson.
There is no agent code to edit. The engine ships primitives — a worker loop, a model client, a set of brokered tools. Everything a person would call "this agent's personality and schedule" is data the engine reads at runtime, never code it compiles in. Change the data, change the agent. The engine doesn't move.
the DEFINITION
1. the declared layer — env vars, org manifests, and run opts — that fully determines which agents exist, who they are, and what each run may do; read by the engine, never compiled into it.
This is the "behavior belongs in the config layer" rule made concrete. The agents lesson drew the trust framing in the abstract; this page is where you turn the knobs. An agent's existence, identity, cadence, budget, and trust are five facts — and all five are config, sitting at three different altitudes.
three ALTITUDES of config
The first thing to learn isn't a knob — it's the map. A given change lives at exactly one of three altitudes, and knowing which one tells you where to make it and when it takes effect:
flowchart TD
subgraph A1["① env — does this agent EXIST?"]
e["WB_CREW_DEF · WB_KEEPER_DEF · WB_AUTOPOET · WB_WEB …"]
end
subgraph A2["② manifests — WHO is it, and WHEN?"]
m["crew.org · lifecycle.org · the agent def (:MODEL: :TOOLKITS:)"]
end
subgraph A3["③ run opts — what may ONE run do?"]
o["model · max_steps · exec · tenant · workdir"]
end
A1 --> W["Worker (the tick loop)"]
A2 --> W
W --> R["AgentDef.run"]
A3 --> R
R --> G["Agent.run — one bounded run"]
style A1 fill:#9fc4e8,stroke:#121316,stroke-width:2px
style A2 fill:#aee5c2,stroke:#121316,stroke-width:2px
style A3 fill:#f2ddb0,stroke:#121316,stroke-width:2px
style W fill:#fbfaf6,stroke:#121316
style G fill:#13d943,stroke:#121316
Read the picture top to bottom. Env decides existence — set the right variable and a worker joins the supervision tree; leave it unset and the subsystem isn't there at all. Manifests — org files — decide identity and cadence: which def an agent runs, how often it ticks, what state machine shapes its days. Run opts decide one run's budget and trust: which model, how many steps, whether it may touch the world. Three layers feed one worker, which calls one run. Nothing in that chain is compiled — every box is the engine reading data.
unset means ABSENT
Start at the top altitude, because it has the sharpest rule in the whole system. When the runtime boots, it asks one question to decide which agent it runs — and the answer is a cascade of env vars:
flowchart TD
boot([runtime boots]) --> q1{WB_CREW_DEF set?}
q1 -- yes --> crew["start Keeper.Crew
(N workers from the manifest)"]
q1 -- no --> q2{WB_KEEPER_DEF set?}
q2 -- yes --> single["start Keeper
(one singleton worker)"]
q2 -- no --> none["NO agent subsystem
in the supervision tree"]
crew -. "crew + singleton
never both run" .- single
style crew fill:#aee5c2,stroke:#121316,stroke-width:2px
style single fill:#9fc4e8,stroke:#121316,stroke-width:2px
style none fill:#e8d3d3,stroke:#121316
The crew takes precedence: if WB_CREW_DEF is set you get the
crew, and the singleton never starts. If only WB_KEEPER_DEF is
set you get the lone keeper. If neither is set, the agent subsystem is
not started at all — it isn't disabled-but-running, it's excluded
from the tree. The crew and the singleton can never both run.
This is one pattern, not a special case, and the engine repeats it
everywhere. The web control plane joins only on WB_WEB=1 (port
PORT, default 4000); the public plane only on
WB_PUBLIC=1 (default 4001); TLS only on
WB_PUBLIC_TLS=1 (4443). The Telegram channel joins only when
TELEGRAM_BOT_TOKEN is present; the
groundskeeper only when WB_GK_SECRET is.
The autopoet is a peer worker behind
WB_AUTOPOET=1 plus a def. The principle is constant: an unset
variable doesn't toggle a feature off — it removes the subsystem from the
running tree. Configuration here shapes what exists, not just how it
behaves.
the singleton's KNOBS
Once a keeper exists, env vars are its full control surface. Every knob has a real default; the def is required to even leave idle. Here is the complete singleton contract, with the numbers the engine actually uses:
| env var | what it controls | default |
|---|---|---|
WB_KEEPER_DEF | path to the agent def — required to activate; unset and the worker starts but idles forever | none → idle |
WB_KEEPER_INTERVAL_MS | tick interval; the keeper never fires at boot — a fresh one waits a 60s grace, a restart waits whatever's left on the clock (see catch-up below) | 3 600 000 (1h) |
WB_KEEPER_RUN_TIMEOUT_MS | wall-clock bound per run; exceeded → brutal kill, outcome :killed | 900 000 (15m) |
WB_KEEPER_MODE | plan or edit — prepended to the task line; the def enforces it, not the runtime | plan |
WB_TENANT | the run's workdir is this tenant's git repo, so its commits ARE the public changelog | local |
WB_KEEPER_CONTINUOUS | 1/true → a short breather between ticks instead of the full interval | off |
WB_KEEPER_BREATHER_MS | breather length in continuous mode | 45 000 (45s) |
WB_LIFECYCLE_DEF | optional state-machine spec; unset or unparseable → plain interval ticks, zero regression | none |
WB_GITOPS | 1 → pull human/CI pushes before each tick and integrate live; conflict → abort, left for a human | off |
The defaults tell a story: a keeper ticks hourly, gives each run
fifteen minutes of wall clock, plans rather than edits, and writes to
the local tenant. Two of those — interval and timeout — are the
cadence and the safety valve, and they interact through catch-up
scheduling.
That scheduling is the subtle part. The last-run timestamp is persisted to
disk (WB_DATA/keeper-last-run). On restart the first delay is
max(60s, interval − elapsed) — a sixty-second boot grace, then
the rest of whatever was left on the clock. Restarts don't reset the
cadence. Redeploy a keeper four hundred times and it still ticks roughly
hourly, because the clock lives in the data, not in the process.
one manifest, many WORKERS
A crew is the singleton's contract, multiplied. One org file — the manifest — lists agents as headlines, each with a properties drawer. Here is a real one: the bit.ml newsroom, four agents, verbatim:
#+TITLE: crew — the bit.ml newsroom manifest * desk :PROPERTIES: :DEF: /data/agents/desk.org :INTERVAL: 45m :END: * moss :PROPERTIES: :DEF: /data/agents/researcher.org :INTERVAL: 15m :END: * wren :PROPERTIES: :DEF: /data/agents/writer.org :INTERVAL: 15m :END: * hale :PROPERTIES: :DEF: /data/agents/editor.org :INTERVAL: 20m :END:
Set WB_CREW_DEF to that file and four workers start. Each
drawer carries :DEF: (required — a member with no def is skipped
and logged), :LIFECYCLE: (optional — absent means plain interval
ticks), and :INTERVAL: (default 1h). The interval grammar is
small and shared: 10m, 2h, 90s, or a
bare number of milliseconds. Want hale to run every ten minutes? Change one
line to :INTERVAL: 10m. No code, no rebuild — the manifest is
re-read from disk.
Two knobs keep a crew from stampeding the model. Stagger
(WB_CREW_STAGGER_MS, default 30s) delays worker i's
first tick by i × stagger — so the four above first fire at 0s, 30s,
60s, 90s, fanning in rather than thundering. Concurrency
(WB_CREW_MAX_CONCURRENT, default 2) caps how many run at once
through a FIFO counting semaphore — the Gate:
sequenceDiagram participant W1 as wren participant G as the Gate (cap 2) participant W2 as hale W1->>G: acquire (slot free) Note over W1: runs — slot held W2->>G: acquire (queued, waits ∞) Note over W2: blocked, FIFO W1->>G: release (in an after — even on crash/timeout) G-->>W2: slot handed off Note over W2: runs now
The release runs in an after clause, so a crashed or
timed-out run always frees its slot — a stuck worker can never
starve the crew. And every worker namespaces its own state by name: wren's
files are keeper-last-run-wren and lifecycle-pos-wren,
its status key is tagged "wren". N agents tick with zero
shared cadence state — the singleton is just this same worker with a
no-op gate and no suffix.
One honest note on the manifest: the def's board-claim protocol — an agent marking a task as its own before working it — is a convention committed to git, not something the runtime enforces. The runtime only isolates each run and throttles concurrency. It will not stop two agents from grabbing the same task; the claim discipline lives in the defs.
the deterministic SKELETON
Depth rung — skippable. Plain interval ticks are "do your job every N
minutes." A lifecycle wraps that loop in a deterministic state machine
so an agent's days have shape while its work stays
non-deterministic. It's an org file too: #+START: names the
first state, headlines are states, and per-state properties draw the edges.
Here is the canonical loop:
#+START: wake_add * wake_add :PROPERTIES: :KIND: wake :REPEAT: 3 :NEXT: wake_audit :END: * wake_audit :PROPERTIES: :KIND: wake :NEXT: rem :END: * rem :PROPERTIES: :KIND: rem :MIN-INTERVAL: 10m :NEXT: wake_plan :END: * wake_plan :PROPERTIES: :KIND: wake :NEXT: wake_add :END:
Read it as a clock face. wake_add runs the agent def
(:KIND: wake) and holds for three successful ticks
(:REPEAT: 3) before advancing. Then one wake_audit.
Then rem — a dream (:KIND: rem, no agent
run) gated to fire at most every ten minutes. Then wake_plan,
and back to the top:
stateDiagram-v2 [*] --> wake_add wake_add --> wake_add: hit < 3 wake_add --> wake_audit: hit = 3 wake_audit --> rem rem --> wake_plan: gate elapsed (10m) wake_plan --> wake_add wake_add --> wake_add: failed / killed (hold) wake_audit --> wake_audit: failed / killed (hold)
The self-loops carry the rule that matters most. A :failed:
or :killed: tick does not advance position — the same
state is retried next tick, so cadence is preserved across crashes. The
position itself, {state, hits}, is persisted to
WB_DATA/lifecycle-pos. If that file holds {wake_add, 2}
and the engine redeploys, it resumes on the second of three adds —
the cadence survives the redeploy. And the wake state's name is prepended to
the task line, so the def picks "add" versus "audit" from the label, not from
counting prose itself. The actual task line an agent receives reads:
MODE: plan LIFECYCLE: wake_audit Perform one keeper run per your loop. …
Worth flagging honestly: the example file marks a per-state
:INTERVAL-MS: override as "advisory," and we won't claim it
changes cadence — the load-bearing time gate is :MIN-INTERVAL:,
which holds position when it hasn't elapsed.
knobs that defend the METER
Several config knobs exist for one reason: every tick can cost an LLM
call, and an idle agent that ticks anyway burns money for nothing. The
sharpest is NO-WORK backoff. An agent signals "nothing to do" by
beginning its result with NO-WORK. Consecutive NO-WORK ticks
back off geometrically — max(base, 60s · 2^(streak−1)), capped
at thirty minutes:
| NO-WORK streak | next tick after |
|---|---|
| 1 | 60s |
| 2 | 2m |
| 3 | 4m |
| 4 | 8m |
| … | doubling |
| capped | 30m |
The verdict of that table: an idle worker quiets itself down toward a
thirty-minute heartbeat — and a single real :done snaps it
straight back to hot cadence. This is the fix that stopped an idle crew from
burning an LLM call every ten seconds to report it had nothing to do.
The other meter-defenders all share the spirit. Continuous mode trades the
full interval for a 45-second breather. The lifecycle's
:MIN-INTERVAL: gates a dream so it can't run hot. The
image tool — when granted — has a hard budget of two
generations per run. And the dreaming model is configured separately
from the waking one: WB_DREAM_MODEL defaults to a smaller,
cheaper model (inception/mercury-2) than the agent reasons with —
config splits the expensive waking model from the cheap dreaming one.
The same budget discipline shows up as max_steps, which
scales with the tenure of the run. A workflow step gets 6. A bare agent run
defaults to 12. An HTTP /api/run or an interactive session gets
40. A keeper tick — a worker with standing — gets 60. A long profile
conversation gets 250. The number isn't arbitrary; it's how much rope the run
has earned.
the trust GRANT
The most misread knob is exec. The reflex reads
exec: true as "let the agent run shell commands." It is the
opposite of that. exec is a trust grant — it adds a fixed
set of host-brokered tools and nothing native. Here is the keeper's
actual run call, with a def whose head declares
:MODEL: xiaomi/mimo-v2.5 and :TOOLKITS: shell wb sandbox:
Workbooks.AgentDef.run(org, "MODE: plan\nPerform one keeper run per your loop. …", exec: true, workdir: repo, tenant: "local", agent: "wren", max_steps: 60)
What exec: true adds is exactly three brokered verbs:
git— the host commits and pushes. The agent names a message; the host runs the commit.publish— the host copies files into the public web root. The agent picks what; the host does the move.image— the host holds the key and the network, and enforces a budget of two generations per run.
That's the entire grant. The native run bash hatch was
deleted — by construction, an agent cannot choose an arbitrary
native command line. Every agent already has a base toolset (an in-WASM
shell, search, fetch, web search, file-issue, vfs read/write, done);
exec only opens the three verbs that touch the world, and it
re-roots the run at the workdir.
sequenceDiagram participant A as the agent participant H as the host participant G as git origin A->>H: git(message: "rem: night audit") Note over H: host runs commit + push
(agent never forks a shell) H->>G: push G-->>H: ok H-->>A: committed
Because this is a grant and not a default, the HTTP surface guards it
hard: POST /api/run honors exec: true only on the
desktop app or with WB_AGENT_EXEC=1 — never for arbitrary,
multi-tenant callers. Trust is something you hand out in increments, and the
increments are these named verbs.
One cautionary detail lives in that same call: tenant: "local".
Omit tenant: and a run defaults to "dev", so its
publish writes to build/public/dev. The lander once
shipped four posts that 404'd for exactly this reason — an untenanted run
published to the wrong directory. Config errors here fail quietly
into the wrong place, not loudly.
where it BITES
Honesty section — config is powerful precisely because it's not enforced everywhere, and you should know the seams.
Some knobs are protocol, not enforcement.
WB_KEEPER_MODE doesn't gate anything in the runtime — it's text
prepended to the task, and the def decides whether to honor "plan" versus
"edit." Board claims are the same: a def-level discipline committed to git,
which the runtime neither checks nor requires. If a def ignores its mode or
skips its claim, the engine won't stop it. These are agreements, and config
expresses them — it doesn't police them.
Tree shape is fixed at boot. The env cascade that decides which agents exist is read when the runtime starts. Editing a manifest updates the data the public plane reads, but re-shaping the running supervisor — adding or removing workers — is a restart, not a hot edit. Treat "who exists" as a deploy-time decision.
Failures are quiet. The tenant-default story above is the pattern:
a wrong or missing value doesn't crash, it routes the work somewhere
plausible-but-wrong. There's a stale doc edge here too — an env reference
still describes WB_AGENT_EXEC as granting a real-CLI escape
hatch, which the code no longer does (it only gates honoring
exec on HTTP). Read the behavior from the running engine, not
the doc.
And the deepest limit: config can make an agent well-governed, not good. It sets the cadence, the budget, the trust, and the boundary. It can't supply judgment, and the pitch was never software that runs itself — these knobs exist so a person can hand a worker exactly as much rope as it has earned, and no more.
questions people actually ASK
Can I change an agent's model without redeploying?
Yes. The model comes from the def's :MODEL: property, read
per run from the def file — the engine is untouched. A caller opt can
override it (the run's model: wins over the def's default), but
the def is the default, and editing it is editing data, not code.
Can two crew agents grab the same task?
The runtime won't stop them — it isolates runs and caps concurrency, but task ownership is the def-level claim protocol, committed to git before work. If you need exclusivity, it lives in the defs, not in a runtime lock.
What happens when a run hangs?
A wall clock catches it: WB_KEEPER_RUN_TIMEOUT_MS, default
fifteen minutes. Exceeded, the run is brutally killed, the tick outcome is
:killed, and a lifecycle holds its position — the same
state retries next tick. A hung run loses a tick, never the cadence.
Is exec safe to grant?
It's a fixed brokered verb set — git, publish,
image — not a shell. The native bash hatch is deleted by
construction, so the agent can't choose an arbitrary command line. On HTTP
it's honored only on desktop or with WB_AGENT_EXEC=1, never for
multi-tenant callers.
Does restarting reset the schedule?
No. The last-run timestamp and the lifecycle position both persist to
WB_DATA. On restart the keeper resumes mid-cadence — "second of
three adds," the rest of the hour still on the clock. The schedule lives in
the data, not the process.
Where do I actually set these env vars?
On the engine you deploy — see the nexus lesson, and
nexus#security for the trust surface. Manifests
and defs are org files on disk; :TOOLKITS:
declarations are covered in the toolkit lesson.
keep GOING
This sub-lesson tunes the agent you already met — the parent comes first.