Local World
Zero-service local development for durable workflow runs.
Little Workflow: published on npm as little-workflow@0.2.0-alpha.1 (2026-10-06). Sections labeled UNRELEASED describe source-only changes. Release status
Local World is the alpha development backend for Little Workflow.
It lets developers compile, run, inspect, and replay workflows without Postgres, Redis, S3, or cloud accounts.
await runWorkflow(myWorkflow, myInput); // world defaults to localWorld()
await runWorkflow(myWorkflow, myInput, {
world: localWorld({ dataDir: ".little-workflow-tests" }),
});Options
type LocalWorldOptions = {
dataDir?: string;
maxConcurrentSteps?: number;
};dataDir— root directory for all world state. Defaults to.little-workflowin the current working directory.maxConcurrentSteps— caps total in-flight step attempts across all parallel groups and serial steps in a run. Effective concurrency at any moment ismin(group.maxConcurrency, world.maxConcurrentSteps - in-flight). Defaultundefined(no cap). For compute-heavy workflows,os.availableParallelism()is a sensible starting value. The cap gates all step executions;parallelcontainer dispatch itself is excluded to avoid deadlock.
import os from "node:os";
import { localWorld } from "@little-stack/workflow";
const world = localWorld({
dataDir: ".little-workflow",
maxConcurrentSteps: os.availableParallelism(),
});Directory Layout
The Local World data directory is self-contained. Events are stored in one SQLite/WAL database keyed by run id; artifacts and outer-loop manifests remain ordinary files.
.little-workflow/
events/
events.db ← SQLite event rows for every run
events.db-wal ← WAL sidecar, when active
events.db-shm ← SQLite shared-memory sidecar, when active
artifacts/
art_123.json
blobs/
art_123.bin
runs/
run_123/ ← per-run scratch + step working dirs
scratch/
steps/
<stepPath>/scratch/
locks/
outer-loops/
ol_abc123.jsonEvents are committed as SQLite rows and exposed through world.listEvents(runId) and little events <runId>. Artifacts live as files on the local filesystem with a small JSON manifest per artifact. Production databases, distributed queues, and remote blob storage are planned post-alpha (see the Roadmap) and later.
events/
Every workflow run, orchestrator run, and sub-workflow run writes to <dataDir>/events/events.db. The event table uses (run_id, sequence) as its primary key, so each run still has an independent ordered event stream even though the bytes live in one database.
For pipeline runs (array of workflows + orchestrator), the orchestrator is the top-level run and sub-workflow runs are linked by events. Sub-run runIds are reserved in the orchestrator's event stream via harness.tool_call.started.payload.args.subRunId and returned via harness.tool_call.succeeded.payload.result.runId.
memory/
Memory stores persist across runs. Three scopes:
| Store | On-disk path | Harness mount | Default mode |
|---|---|---|---|
| Workflow | memory/workflows/<workflowId>/ | /mnt/memory/workflow/ | rw by that workflow's planners; ro by workers |
| Pipeline | memory/pipelines/<pipelineKey>/ | /mnt/memory/pipeline/ | rw by the orchestrator; ro by planners + workers of sub-runs |
| Org | memory/org/ | /mnt/memory/org/ | ro by all roles (override requires memory: { org: "rw" } on the workflow) |
<pipelineKey> is a hash of the orchestrator's sorted set of available WorkflowDefinitionHash values. Runs with the same pipeline composition share a pipeline memory store.
Memory stores are created lazily on first write. Workflow memory is keyed by workflowId; changing the workflow id creates a new store. Devs may pre-populate the org store with policies, runbooks, or style guides before running any workflow.
See Memory for the memory model, mount semantics, and memory poisoning mitigations.
outer-loops/
When maxOuterCycles > 1, the runtime writes a manifest per outer-loop session:
// outer-loops/{outerLoopId}.json
{
"outerLoopId": "ol_…",
"goal": { "workflowDefinitionHash": "…", "description": "…" },
"maxCycles": 5,
"inputHash": "sha256:…", // identity of the loop's input; a resume with other input is rejected
"cycles": [
{
"cycleNumber": 1,
"runId": "run_…",
"workflowVersionId": "wv_…",
"status": "completed",
"output": { }
}
],
// pendingCycleRunId is set before a cycle starts and cleared after supervise completes
"pendingCycleRunId": "run_…",
// result is set when supervise returns { kind: "done" }
"result": { "kind": "done", "finalOutput": { } }
}The manifest is written via atomic rename (write to *.tmp, rename into place) after every OuterLoopCycleCompleted event. See Durability for the resume scenarios this enables.
API Shape
Code should depend on runtime APIs, not the on-disk layout:
runWorkflow()localWorld()- the World methods:
world.listEvents(runId),world.appendEvent(...),world.readArtifact(ref) replayRun()
The on-disk layout described above is an implementation detail. It grew to support memory stores and nested sub-runs, but the API surface is stable. Store interfaces, SkillStore, and PluginStore arrive in later versions when production adapters, skills, and plugins exist.
Limits
Local World is not production-grade:
- no distributed workers
- no production locking
- no multi-tenant isolation beyond directory separation
- no presigned multipart upload flow
- no production queue backend
- memory stores are plain files with no versioning, audit log, or concurrent-write protection (single-process assumption)
The first alpha demo proves kill-and-resume behavior: stop a run mid-flight, restart, replay persisted events, and continue without re-calling completed model or tool steps. The second demo exercises orchestrator + memory: an orchestrator reads accumulated pipeline memory on each run, writing better plans over time.