Workspace & state

Every run gets a /workspace. It's where your program runs, and it's scratch unless you say otherwise. That's the whole model: a run does its job and leaves nothing behind, unless you name the part that should outlive it.

Naming that part is how a workflow gets better over time instead of starting cold every morning: a cache it reuses, an index it extends, notes an agent keeps.

You run in the workspace

Your program's working directory is the workspace (and so is HOME), on every runner: hosted runs and your own self-hosted runners. So a relative path needs no ceremony:

import { writeFileSync } from "node:fs";

writeFileSync("notes.md", "hello");   // → the run's workspace

When the run ends, that file is gone; the machine is destroyed. For most workflows that's right. If you want an absolute path (to hand to a tool, or to log), context.workspaceDir, on your runfunction's second parameter, is the same place.

Making things stick

Name the directories that should survive. They're restored before your program starts on the next run:

workflow.jsonc
{
  "slug": "triager",
  "triggers": [{ "kind": "cron", "expr": "0 9 * * *" }],
  "workspace": { "persist": ["cache", "state"] },   // these come back; everything else is scratch
}
OptionWhat it does
"persist": ["cache", "state"]Restores the named directories before the next run; everything else stays scratch.
"persist": trueKeeps the whole workspace. Convenient, but every run hauls all of it to storage.
"key"A ${input.<path>} template that splits the persistent workspace, one state per resolved value (below).
Environment scopingAutomatic, no config: each environment keeps a separate workspace; runs with no environment share a base one.
Size capA snapshot is capped at 512 MB and counts toward your org's storage quota.

{ persist: true } is the one that bites: if your workflow clones a repo or runs npm install, it hauls all of that to storage on every single run, toward the size cap and your storage quota. A workspace is mostly scratch with a small part that compounds. Say which part.

Agent memory

An agent() call can keep its own notes, and that directory persists automatically, with no workspace declaration needed:

await agent("Triage today's failures. Note anything you learn.", {
  memory: "triager-notes",
});

The agent gets file tools scoped to that directory and reads it at the start of each turn, so next run it picks up where it left off. Your program code can read and write the same files (triager-notes/…). Point two agents at the same path to share what they learn, or separate paths to keep them independent.

One workflow, many states

One workflow often serves many subjects: a repo each, a customer each, a tenant each. Persisting into a single shared workspace would blend them together, and deploying N copies of the same program to keep them apart is worse. key splits the persistent workspace instead, so one workflow keeps N independent compounding states:

workflow.jsonc
{
  "slug": "repo-maintainer",
  "triggers": [{ "kind": "github", "event": "pr.merged" }],
  "workspace": {
    "persist": ["index"],
    "key": "${input.repo.fullName}",   // one index per repository
  },
}

It is a template over the run's input, the same ${input.<path>} grammar as concurrency.key. The syntax is checked at deploy; the value resolves at run creation as plain data access, with no program code involved. Two runs whose keys resolve to the same string share a workspace; different keys never see each other.

The two keys are independent axes that happen to share a grammar. This one answers "which state do I compound into?"; concurrency.keyanswers "how many runs may execute at once?" Set either, both with different templates, or neither.

One workspace per environment

If you run the same workflow in staging and production, each keeps a separate workspace. They never see each other's state.

That matters because an environmentis chosen when the run is triggered, not in your code: one program, several environments, different secrets and variables. Staging's cache has no business becoming production's starting point. Runs with no environment share a base workspace.

When it saves

MomentSaved?
End of a runYes
End of a failed runYes, on purpose
Before a long sleepYes
Hard crashNo; the next attempt starts from the last save

Failures persist on purpose. If a run spends twenty minutes learning something and then dies on the last step, you want that next time, not a blank slate. The cure for bad state isn't throwing away good state; it's reset.

The exception is a hard crash: if the machine dies outright, nothing is saved. So treat persistence as saved at checkpoints, not saved continuously, and checkpoint expensive side effects (push commits incrementally) rather than relying on the workspace to hold half-finished work.

When state goes wrong

It will, eventually: a cache poisons, an agent learns something untrue, a failed run leaves a half-written index. So you can look, and you can clear:

$ boardwalk workspace show triager
Persistent workspace · triager

  production           4.2 MB   last written 3h ago
  (base)               275 B    last written 2d ago

$ boardwalk workspace reset triager --environment production --yesreset the persistent workspace of triager for production.

Reset clears only the state: the workflow, its triggers, and its run history are untouched. That's the point of it existing separately from deleting the workflow. It's on the workflow's dashboard page too, and available to agents over MCP.

It clears one scope at a time. --environment picks the environment and --key the workspace key, so a poisoned cache for one customer is one reset, not a wipe of every customer's state. Leave off --yes and it prints what it would clear without touching anything.

One caveat: it doesn't interrupt a run already in flight. That run holds its own workspace and saves at the end, overwriting your reset. Disable the workflow first if it's on a schedule.

Sharing, size, and where it lives

  • Two runs at once share one workspace, last-writer-wins. If your state can't tolerate tearing, set "concurrency": { "mode": "serial" } in workflow.jsonc.
  • It counts toward your storage.A snapshot is capped at 512 MB, and what you keep is part of your org's quota, another reason to name directories rather than keep everything.
  • Self-hosted keeps its own. A self-hosted runner persists to its own disk; we never store your workspace. The semantics are identical; only the location differs.
  • It isn't a shared filesystem.Each workflow (and each environment) gets its own; runs don't read each other's. To publish a file out of a run, use artifacts.

If you keep one thing: ask what this workflow should know tomorrow that it doesn't know today. Persist exactly that. A cache, an index, a memory; not a repo you can re-clone, and not node_modules.