owenloop
v0.5.30
Published
Deterministic rails for agentic workflows. Declare steps and dependencies; the engine guarantees order, redoes what changes invalidate, and stops what keeps failing — instead of hoping the agent follows through.
Maintainers
Readme
owenloop
owenloop is deterministic rails for agentic workflows.
Most agent workflows today run on hope. You write careful instructions in a prompt or a skill — update the state file, run the verifier, stop when it's done — hand them to an agent, and trust it to follow through. Sometimes it does.
owenloop replaces the hope with guarantees. You declare the steps and what each depends on; the engine enforces the rest. A step runs only when everything it needs is actually done. When a result changes, everything built on it is invalidated and redone. When a step keeps failing, it's stopped and flagged for a human instead of retried forever. The agents stay probabilistic — that's what makes them useful. The workflow around them doesn't.
See it work
A research pipeline: a researcher gathers findings, a writer turns them into a report, and an independent reviewer must sign off before the report counts. Every clause of that sentence is enforced below, not requested:
# workflows/report.yaml
name: report
inputs:
- name: question
seedOwed: true
steps:
- name: researcher
consumes: [question]
produces:
- name: findings
schema: { type: object, required: [claims, sources] }
body: Research the question. Every claim needs a source.
- name: writer
consumes: [findings]
produces:
- name: report
judges:
- name: reviewer
body: Reject the report if any claim lacks a citation
or drifts from the findings. Otherwise approve.
maxAttempts: 3
body: Write the report from the findings, citations inline.You don't drive this by hand. An orchestrator — an agent skill, a plain
while loop, your own code — ticks the engine, hands each job to a Step Agent,
and reports results back. Here's what the engine enforces as that loop runs:
The writer cannot start early. The first tick emits exactly one order — the researcher's. There is no writer job to hand out, so no eager Step Agent can write a report from findings that don't exist. The hope version is a prompt: "wait until research is complete before writing."
Malformed output never enters the pipeline. The researcher reports findings missing
sources— the engine refuses the commit at the schema, and the retry order carries the validation errors as feedback. The hope version: the bad output flows downstream and fails somewhere confusing.The report can't approve itself. When the writer commits
report, it lands insubmitted, not done. The reviewer is a separate order to a separate Step Agent — one that never saw the writer's reasoning. A rejection re-arms the writer with the reviewer's reasons attached to its next job. The hope version: "review your work before finishing" — the fox auditing the henhouse.Failure has a floor. Third rejected attempt and
reportstalls: the engine stops issuing jobs for it and flags it for a human, instead of letting an agent grind the same mistake all night on your API bill."Done" stays honest. Sharpen the
questionafter the run finishes andfindingsandreportfall back to owed — the finished workflow un-does itself, automatically, rather than standing on inputs that no longer exist.
None of this depends on an agent reading carefully, remembering instructions, or being honest about its own work. And notice that you never defined a state or a transition — only what each step consumes and produces. This isn't a state machine an agent is asked to role-play from a prompt; the states live in the engine, and the engine doesn't negotiate. The agents stay probabilistic; the bookkeeping around them never is. The Quick start gets you running in two commands, and Driving it with a loop covers orchestrators — including the shipped skills that do it for you.
Why it exists
Agents are good at doing one task. They're bad at the bookkeeping around a task: remembering what's already done, noticing when an earlier step's output changed, retrying the right number of times, and knowing when to stop. Wire a few agents together by hand and you end up writing a pile of glue — who runs next, what to re-run when something upstream moves, when to give up and ask a human.
owenloop is that glue, written once and tested hard. You declare the steps; it handles the three things that are tedious to get right:
- What runs next. A step is ready the moment everything it depends on is accepted and it still owes an output. That's the whole scheduler — there's no status field to flip, nothing to sequence by hand.
- What to re-run. Change an early step's output and everything built on it automatically falls back to "not done." No manual invalidation, no stale results slipping through.
- When to stop. If a step keeps getting rejected past its limit, owenloop stops re-running it and flags it for a human — instead of looping forever burning tokens.
Hope is not a control flow.
The mental model: owed, not done
The checklist your agents can't cheat.
owenloop doesn't track whether a step is "running" or "done." It tracks what each step owes. Every output is in one of six states:
| state | still owed? | meaning |
|-------------|:-----------:|------------------------------------------------------------------|
| owed | yes | declared but not produced yet, or re-armed — the step owes it |
| green | no | accepted; satisfies everything downstream that depends on it |
| rejected | yes | produced, then judged unfit (or knocked back by a change) — a debt |
| retracted | no | a member dropped from a collection; gone for good |
| skipped | no | a step declined its own output on a dead branch |
| submitted | no* | produced, awaiting sign-off from one or more declared judges |
* submitted isn't a producer debt — the producer already did its job — but the
workflow isn't done while it sits there either. See judges.
A step is eligible to run when it owes a debt (an owed or rejected output)
and every input it consumes is green. Status is never stored — it's computed from
these states on every read, so it can't drift out of sync.
Two things make this more than running steps in dependency order:
- Outputs stay honest as inputs move. A green output counts as done only while the inputs it was built from are still green and unchanged. Re-run an early step and everything built on it quietly falls back to a debt — no code required to invalidate it.
- Rejections carry reasons. When a reviewer rejects an output, the text rides along. The next job for the producer shows why it's being asked again, so the Step Agent has the feedback in hand. (Three flavors: a reviewer's judgment, the engine's own schema refusal of a malformed value, and structural knock-backs from a change cascading downstream.)
What owenloop is not
- Not a scheduler with its own clock.
cadence:andmaxRunsPerDay:cap how often an eligible step can fire, but nothing in owenloop wakes up on a timer — the outer loop (see below) is what initiates every tick. - Not shared state across instances. Every workflow instance is its own
island — artifacts, tasks, and runs are all scoped to one instance. The one
deliberate exception is the
calls:/producedBylink between a parent instance and the child it explicitly spawned. - Not a dynamic graph at runtime. Collections give a workflow dynamic width — a producer can emit any number of elements — but the wiring graph itself (which steps exist, what each consumes and produces) is fixed when the definition loads, not mutable while an instance runs.
- Not a command runner.
executor:/command:declare which kind of worker an order is for (Order.worker) and what command text a worker resolves for it — the engine never shells out, executes, or interprets it. Actually running anything is always the dispatcher's job, on the other side oftick.
It scales up when you do
Each of these is a small addition to the base model above — most workflows use only a few. Skim the ones that sound relevant; every entry links to the full reference.
Judges — enforced independent verification
A produces: entry can declare one or more judges: deterministic quality
bars an artifact must clear before it counts as green. This is the
independent-verifier pattern, enforced structurally instead of remembered as a
convention: the artifact lands in submitted, not green, and cannot move
further until a separate order — with no view of the producer's reasoning —
signs off. A rejection carries its reason back to the producer's next job, the
same as any other knock-back. See docs/authoring.md
and judged-research.yaml.
Durable by default
State lives in a single SQLite file, not in a session or a context window.
Kill the process, come back next week, run owenloop tick — the engine knows
exactly what's owed and picks up where it left off. The workflow outlives the
process, the session, and the model that's driving it. See
Storage.
Stall detection — the token-burn stopper
If an output is rejected more times than its maxAttempts, the engine stops
re-arming it. It stays a debt, but produces no more jobs — the step has
demonstrably failed, and it's flagged for a human instead of looping forever.
owenloop retry clears the stall and resets the counter, optionally with new
guidance. maxAttempts (and maxSchemaFailures below) is set on the step as
a default for all its outputs, but any single produces: entry can override
either cap for itself when one output needs a different bound than its
siblings. See docs/cli.md and
docs/design.md §6.
Schema refusal
A produces: entry can carry a JSON Schema; a green/emit/seal whose
value fails it is refused at the engine, not silently accepted and discovered
downstream. Repeated schema failures trip the same stall mechanism as
judgment rejections. See docs/authoring.md.
Cascade invalidation
Think of it like a build system: change a header file and make knows every
object file that includes it needs recompiling, without you tracking that by
hand. owenloop does the same thing for agent outputs — change an early step's
result and everything built on it automatically falls back to "not done," and
gets redone the next time its inputs are green. No manual invalidation code,
no stale results slipping downstream. See
docs/design.md §7.
Collections — fan-out/fan-in
A step can emit any number of elements at runtime; a map step runs once per
element, and a reduce step runs once after they're all in (or, with a
suffixed reduce, once every element's own per-element output is in). See
docs/authoring.md and
research.yaml. A rejected collection
seal can be re-emitted and resealed; rejected members are recovered by an
authorized retract, as described in the authoring guide.
Composition — include: and calls:
Build a workflow out of other workflows two ways: include: splices another
def's steps directly into the parent at load time (one flat graph); calls:
delegates to a separate child instance at runtime, keeping its internals
hidden as a black box. See docs/authoring.md.
Side-effect policies — effect:
Most steps are safe to re-derive when their inputs move — that's what the
cascade assumes by default. A step with an irreversible side effect (a
deploy, a publish, an external write) can declare effect: { idempotent:
false, onInvalidate: … } to tell the engine to pin the old result, escalate
to a human, or run a compensating step instead of silently re-firing. See
docs/authoring.md.
Model tiers
model: fast | standard | strong | strongest declares intent, not a vendor
id — the engine passes it through untouched to whatever dispatches your
Step Agents. A portable workflow says "this step needs strong judgment"; the
host binds that to whatever model it runs on. strong is a high-capability
workhorse tier, not the host's single most capable model — that's what the
opt-in strongest tier is for, reserved for the rare step where nothing less
will do. See
docs/authoring.md.
Worker dispatch
executor: agent | command | … declares which kind of worker a step's order
is for — the authored value lands on the order as Order.worker, and the
default (agent, silent when omitted) is unaffected; every def written
before this feature stays byte for byte the same. Opt a step into
executor: command and give it a command: string to switch it to a
deterministic worker instead of an LLM — the engine never runs it; your
dispatcher branches on order.worker and resolves the authored command text
from the local workflow store through the order's defDigest at the
(defDigest, step, key) boundary. The packet itself carries no command text:
a command worker must refuse when the digest or command cannot be resolved and
must never spawn from transport-supplied text. An optional spec: map carries
further opaque config (a timeout, a working directory), and a judge entry
accepts the same fields, so a quality gate can be a script's exit code instead
of a verdict. See
docs/authoring.md and
command-executor.yaml.
Event subscription — for embedding
Driving the engine in-process doesn't require polling: engine.subscribe(...)
pushes a typed event the instant a mutation commits, so a host can react
instead of ticking on a timer. See Embedding it and
docs/embedding.md.
Shared workflows — the content-addressed store
Workflow definitions are shared, not hand-copied. Two install routes land them in the tree, both all-or-nothing with crash recovery:
- GitHub repos —
owenloop add <owner>/<repo>[@ref]fetches a public repo'sworkflows/**, validates every def with the engine's strict pass, and installs it under<defsDir>/<owner>-<repo>-<hash>/, recording provenance (resolved commit sha) in.owenloop/installed.json. - Workflow bundles —
owenloop add widget.wnlp [--global](a local.wnlpfile or anhttps://URL) installs into a content-addressed store: the project store lives under the defs dir,--globalinstalls into<home>/.owenloop/workflows. Each store holds anindex.jsonmappingnamespace/name@versioncoordinates to content digests, plus immutable objects atobjects/sha256/<digest>/— identical content deduplicates to one object, and an object's identity is its digest alone (never its name or source). Objects are hardened read-only in place, and every resolution re-verifies the bytes before returning a path. Execution workers resolve static prompt and command text from those verified local objects by the order'sdefDigest; a remote packet cannot substitute instruction text.
The bundle route derives <home> from the first non-blank caller-injected
HOME or USERPROFILE value (HOME wins), and uses that home for the default
recovery-marker directory. A bundle install refuses when neither variable is
supplied rather than falling back to the process user's ambient home.
Normal definition discovery no longer floods stderr for expected bundle
history. It emits at most one note: N superseded bundle versions hidden;
--verbose to list them; the global --verbose flag restores every detailed
superseded-version notice. The actionable has no selectable version warning
for competing non-SemVer versions is never hidden.
Store history can be bounded safely with a dry run first:
owenloop bundle gc --keep 2 # project store, report only
owenloop bundle gc --keep 2 --yes # recompute under lock and apply
owenloop bundle gc --global # global store, report onlyThe default keep count is 2 (current plus one rollback version). GC preserves
selected versions, explicit pins, retained local instance snapshots, transitive
bundle locks from every retained coordinate in either root, exact versioned
calls from retained legacy snapshots and current project/add definitions, and
project-index fallback into the global store. Reachability is coordinate- and
root-aware, so
an intact non-target copy does not keep a redundant target history beyond
--keep. It never contacts the hub; --yes is the only destructive switch
and only the target bundle index/object tree is changed. Applied GC takes both
roots' writer locks (creating only coordination state at a known missing
counterpart root when necessary). Project GC also takes the legacy GitHub-add
lock and recovers its journal before clearing their shared staging tree; global
GC uses that lock as a read barrier without recovering or otherwise mutating the
project tree.
CAS-backed snapshot writers share the store locks, and bundle installs
revalidate exact manifest locks before commit, so stale definitions or callers
cannot be pinned during or immediately after collection.
Bundle installs fail closed without their two required adapters (bundle
ingestion and pre-commit verification). The default CLI binds the real
bundle ingestor, so a .wnlp produced by packBundle reaches the normal
entrypoint-aware install validation; the pre-commit verifier remains unbound,
so the CLI still fails closed rather than accepting an unverified bundle. There
is no default accepting signature verifier; the add route does not consume
publication sidecars or verify a bundle's signature. Use owenloop publish to
create the .wnlp bundle plus a signed publication sidecar and optional signed
origin sidecar, or an explicitly unsigned marker.
--global applies only to .wnlp bundle sources; a GitHub source with --global
is refused before any network request. Resolution is
deliberately split: execution resolves by digest only (project first,
fall-through to global only when the project object is absent — a corrupt
project object is a hard error, never masked by a global copy), while
human-facing lookups resolve by coordinate and surface a structured
ambiguity error when the two levels disagree — never a silent project-first
pick. Full semantics:
docs/cli.md.
Driving it with a loop
owenloop never runs anything itself. It hands out jobs and waits to hear back —
something has to tick it, run the work, and report the result. That something can be
as simple as a while loop around an agent. The Ralph
loop — keep an agent ticking with a fresh context
each pass — is exactly this kind of outer loop, and owenloop is the half it's
missing: the persistent state and the brakes. The loop keeps going; owenloop
remembers what's owed, what failed and why, and when the whole thing is actually
done. They work side by side — the loop is the muscle, owenloop is the memory.
The outer loop is deliberately not owenloop's business, which means it can be anything that can run a CLI command or call a function. In practice that looks like:
- Your own harness — a
whileloop, a cron job, a CI stage: tick, run each order with whatever executes your work (an agent CLI, an API call, a script), report, repeat. Fully deterministic dispatch if you want it — see Embedding it for the in-process version. The rootowenloop shiftcommand is the standing Shift runtime — the engine core still ships no host harness of its own. - A Prime Agent as the orchestrator — point any tool-using agent (Claude Code, Codex, Gemini CLI, anything that can run a shell command) at the CLI and tell it to drive the instance to done. A slash command or skill that wraps this turns "run the release workflow" into one line.
- A Prime Agent structuring its own work, inline — mid-task, the Prime Agent authors a throwaway workflow, drives itself through it, and deletes it: the engine as scratch discipline rather than standing infrastructure.
The engine doesn't know or care which of these is ticking it — an order is an order. The Owenloop Claude Code and Codex plugins provide six hub-native skills:
conduct— supervise one existing workflow through a scoped Shift, check its status, and relay human gates.author— interview a human, draft a workflow, validate it throughcreate_workflow, present it for approval, and offerstart_run. Useconductorshiftto supervise the resulting run.ephemeral— structure the caller's own one-off work with an ephemeral workflow.graduate— turn evidence from a completed ephemeral composite into a reusable, policy-conformant library candidate.plan— compile novel multi-domain work into a checked, approval-gated ephemeral composite that delegates library playbooks.shift— attend a crew's ongoing workflow workload through the blocking Shift loop and relay human gates.
Put the owenloop CLI on PATH and run owenloop setup; the setup flow converges
the bundled Claude Code and Codex plugins on that same CLI and hub connection.
Quick start
Put the owenloop CLI (Node ≥ 22.13) on PATH and run owenloop setup once. Then
ask your Prime Agent for what you want:
Use author to build me a workflow that researches a topic, writes a report, and doesn't accept it until an independent reviewer signs off — then run it on "tidepools".
The author skill interviews you for anything missing, writes and validates the
YAML definition, and presents the process for approval before offering to start it.
Already have a def? "Conduct the report workflow" hands it to
conduct. The shipped Claude Code and Codex
plugins launch the same owenloop command as
owenloop mcp, rather than using a separate npx-pinned server. The plugins
check the installed CLI/plugin version pair at session start when their hooks
are enabled, and the MCP server reports a readable mismatch error on tool calls; run owenloop setup when the versions differ. For one-off shell
use, npx owenloop still works — no clone, no build, no environment
variables, no CLI verbs to memorize. The same package also includes the
execution-side companion; use owenloop work <subcommand> for lower-level
execution tasks. There is no separate owenwork package or binary.
Deterministic workflow bundles
A workflow and its referenced regular files can be packaged as one deterministic
.wnlp file. The package format uses a canonical POSIX/PAX tar stream wrapped
in gzip; the package digest is SHA-256 over the exact uncompressed canonical
tar, not over the gzip bytes. bundle.yaml carries package identity, platform
selectors, requested capabilities, generated per-file SHA-256 values, and
namespace-qualified, digest-pinned calls: references. workflow.yaml remains
the only execution definition.
owenloop bundle pack ./report --output ./report-1.2.0.wnlp
owenloop bundle inspect ./report-1.2.0.wnlp
owenloop bundle digest ./report-1.2.0.wnlp
owenloop bundle unpack ./report-1.2.0.wnlp ./unpacked-reportThe bundle format commands (pack, inspect, digest, and unpack) are
filesystem-only: they do not open or create the local .owenloop/state.db and
do not contact a remote service. bundle gc is intentionally store- and
local-snapshot-aware, but likewise never contacts the hub. Packing never edits
the source manifest. Inspection and unpacking reject unsafe paths, symlinks,
unsupported archive entry types, malformed canonical headers, duplicate files,
invalid manifests, workflow/manifest name mismatches, missing lock entries, and
integrity mismatches before files are written. See the bundle format
reference and the CLI bundle commands.
For a foreground local shift, start the daemon in one terminal. Run the client commands from another terminal:
# Terminal 1: foreground daemon
owenloop shift start alpha
# Terminal 2: client calls
owenloop shift next
owenloop shift status
owenloop shift endshift start requires at least one named crew unless you pass --all
explicitly; --all serves all Crews for the Scoped Identity. shift next waits
up to 90 seconds by default. For flags, JSON output, daemon behavior, and exit
codes, see the shift reference.
A running shift also writes to disk: its own dispatch record as JSON Lines in
shift.log, and each dispatched worker's stdout and stderr in <run>.log. Both
outlive the shift, which is what makes a postmortem possible after a shift dies.
See docs/shift-logs.md.
Want to see or drive the machinery yourself? Everything above goes
through the same small CLI (create, tick, green, reject, …).
docs/cli.md has the full command reference and a hand-driven
walkthrough of a pipeline — including a rejection knock-back and a stall —
and examples/workflows has seventeen runnable defs, from a
minimal review loop written to teach the wiring
(delivery — a four-step example, not the
production line) to a production-shaped pipeline
(ship), a
collections-heavy research pipeline
(research), and a compiler that turns
live vendor docs into a new gate-checked def
(compile-dev-playbook).
Full YAML grammar:
docs/authoring.md. Driving it from your own code:
Embedding it.
Want someone else's workflow defs instead of writing your own?
owenloop add <owner>/<repo> fetches a public GitHub repo's workflows/**,
validates every def, and installs them locally with pinned provenance — see
the add entry in docs/cli.md. An installed def's steps later
run with your Step Agents' full privileges and owenloop never executes them itself,
so install only sources you trust, and pin a commit SHA (@<sha>) for anything
you re-add — the trust model
is spelled out there.
Publishing your defs to a hosted hub instead? owenloop login authenticates
the CLI (loopback OAuth, or a pasted token via --with-token; the credential
goes into the macOS Keychain or a 0600 file, never the repo, and is verified
against the hub before it's ever stored), owenloop connect binds the project
to a hub, and owenloop push publishes your defs. Use owenloop push --bundle
<bundle.wnlp> after owenloop publish when execution must be pinned to the
exact signed bundle digest; plain YAML pushes remain idempotent against the
hub's own def hashes. To publish an outside repo's defs, owenloop install
<owner>/<repo> scopes every capability they author to <defName>.<capability>
unless you deliberately map it onto a name your crews already serve — see
install.
owenloop agent new
<name> mints a Scoped Identity on the hub and stores its token in slot
agent:<name> without ever printing it. See the
Hub section in docs/cli.md.
Setting up a machine from scratch? owenloop setup may sign you in as a human
and, when needed, mint or rekey and store a Scoped Identity. Setup writes only
hubOrigin into the execution settings file, preserving the other keys. The
file is $HOME/.owenloop/settings.json by default (OWENLOOP_CONFIG_DIR can
provide an absolute isolated directory; XDG_CONFIG_HOME is not consulted).
For
a non-default account,
setup only prints the OWENLOOP_ACCOUNT=<name> instruction. Setup probes and,
when needed, converges the bundled owenloop plugins for Claude Code and Codex.
Plugin convergence is non-fatal; a missing harness or failed plugin command does
not fail setup. A second run with the expected plugin version already installed
performs no plugin writes when its effective MCP launch is verified safe.
owenloop doctor is the read-only counterpart: it checks both harness plugin
states, including the effective owenloop mcp command and arguments, Codex's
PATH pass-through, and PATH availability. It reports same-version absolute or
worktree launch declarations as drift, but does not treat a plugin's absolute
marketplace or cache location as a launch verdict. A known unsafe declaration is
repaired by the existing non-fatal owenloop setup convergence flow. See
setup and
doctor in docs/cli.md.
Driving the hub from an MCP host instead? owenloop mcp serves the hub control
plane to a local MCP host (Claude Code) over stdio — MCP hosts spawn it, you
don't run it yourself — authenticating as your logged-in human credential and
never surfacing a token to the model. See the
mcp section in docs/cli.md.
Requirements
- Node ≥ 22.13. Storage is Node's built-in
node:sqlite, which is available unflagged from 22.13 onward (it still prints an experimental warning until it stabilises in Node 24.15 / 25.7). owenloop is an ESM-only package. CI runs the full check on Node 22 (active LTS) and 24 (current). - No native dependencies.
node:sqliteis built in, so there's nothing to compile. The only runtime deps areyaml(parsing defs) and@cfworker/json-schema(optional per-artifact schema validation). - OpenSSH
ssh-keygenwith-Ysupport (OpenSSH 8.1+) for the signing features.owenloop setupautomatically ensures an Ed25519 signing key for each of the three local principals — human, machine, agent — and signs / verifies records with stock SSHSIG (ssh-keygen -Y). Keys land in the macOS Keychain, Linux libsecret (secret-tool), or a0700/0600file store under$HOME/.owenloop/keys/; one backend is chosen once and never error-fallback. See Signing and key storage.
npm install owenloopimport { createEngine } from 'owenloop'; // see "Embedding it" belowUpgrading from 0.2.1
Upgrading a project from the 0.2.1 release pulls in everything that has landed since. The changes that need operator or embedder attention:
- Deep recursive ticking. One
ticknow advances the wholecalls:tree by default, so a single call can drive many workflow instances at once and their execution timing shifts. Embedders that key per-workflow logic — checkpoints, transactions, counters — must key onorder.workflow, not the id passed totick(--shallow/{ deep: false }is the single-instance escape hatch). See Migrating from shallow ticking and Deep tick andorder.workflow; the full guidance lives there. - Node ≥ 22.13 required. The
enginesfield pins Node ≥ 22.13, and CI runs the full check on Node 22 and 24 — see Requirements just above. - Database schema v9. The first open by a new binary migrates the database
in a single transaction that rolls back on failure; a database whose stored
version is newer than the binary is refused rather than opened. Migrated
databases do not get backfilled artifact-history payload snapshots —
durable payload history begins after migration (legacy lifecycle reasons are
carried over exactly once). Copy the SQLite database (
.owenloop/state.dbplus its-wal/-shmsidecars) before upgrading; downgrading means restoring that copy, because an older binary refuses a v9 database. - New retained plaintext data. Issued order packets and artifact history now persist in the database, so rotating and disposing of that data is the operator's job — see Retention and disposal.
- New network-facing commands and credential storage.
owenloop login/connect/push(andadd, for installing shared defs) reach the network and store a credential in the macOS Keychain or a0600file — see the Hub section and theaddtrust model. A hub origin now holds one credential per named slot (--as human|agent|agent:<account>), and credentials stored under the earlier keying are not read — there is no migration, so re-runowenloop login.owenloop agent new <name>mints an agent token straight into slotagent:<name>without ever printing the secret. SettingOWENLOOP_CREDENTIAL_COMMANDinstead supplies the credential from a command of your own (a secret manager, or any host without a keychain); it takes precedence over both stores and fails loudly rather than falling back. Embedders can now read, write, and refresh a hub credential programmatically through the package's exported credential surface (readStoredCredential,storeCredential,deleteCredential,ensureFreshOAuth,CredentialIO); concurrent OAuth refreshes are serialized by acredentials.lockfile so a token rotation can't clobber a fresher one — see What's exported. - Max-lease cap is now opt-in. There is no default lease ceiling; anyone who
relied on the brief post-0.2.1 default cap must set
maxLeaseMs(or a per-stepmaxLease) explicitly.
Embedding it
The CLI is a thin adapter: it maps argv to engine calls and prints JSON. The engine
is an ordinary class, so you can drive it in-process and get typed objects back
(Order, CommitResult, WorkflowStatus) — no subprocess, no JSON parsing.
import { createEngine } from 'owenloop';
const { engine, store, resolver } = createEngine({
db: '.owenloop/state.db', // or ':memory:' for an ephemeral instance
defsDir: 'workflows', // load YAML defs from a dir … or pass `defs: [myDef]`
});
// start an instance (proposal is seeded as owed, so provide it up front)
const wf = engine.createInstance('delivery', {
provide: { proposal: { text: 'add dark mode' } },
});
// the Step Agent loop: tick → resolve → run → report
const { orders } = engine.tick(wf);
for (const order of orders) {
// orders are reference packets: routing + dynamic data + a defDigest, never
// authored prompt/command text. Resolve before dispatch — same boundary the
// CLI uses; an unknown digest throws UnknownDefDigestError here.
const instructions = resolver.resolveOrder(order); // { prompt?, command? }
const result = await runYourAgent(order, instructions); // ← your domain
engine.green(order.workflow, order.run, order.outputs[0], result); // typed CommitResult back
engine.close(order.workflow, order.run);
}
engine.status(wf); // typed WorkflowStatus: done / debts / eligible / blocked
store.close(); // on shutdownPrefer to react instead of poll? engine.subscribe(listener) (or
createEngine({ onEvent })) pushes a typed event the instant a mutation commits — so
you can re-tick only when there's new work, or resolve a promise when the workflow is
done. See examples/events.ts.
The engine/store pair is meant to be long-lived (one per database). Concurrency is
the store's job: node:sqlite is synchronous and single-writer-per-process, and
cross-process safety comes from a commit fingerprint check (described under
Storage). See docs/embedding.md for the full
surface, lifecycle, and trade-offs.
How it's built
owenloop is small and split along a pure-core / imperative-shell line:
| module | responsibility |
|---|---|
| src/types.ts | shared types: the six-state lifecycle, reason threads, def shapes |
| src/paths.ts | parse/match the src[$i] / src[*] / src[] path grammar |
| src/defs.ts | load YAML → validated WorkflowDef (the static wiring checks) |
| src/schema.ts | JSON Schema validation of artifact values, via @cfworker/json-schema |
| src/model.ts | the pure core: what's eligible, the cascade, status, stall detection |
| src/store.ts | the SQLite runtime store — node:sqlite persistence; transactions; the commit check (instance/artifact state) |
| src/install.ts | the host-neutral install transaction: safe staging, atomic swap with a retained backup, two-phase journal, lock, recovery |
| src/store/ | the content-addressed workflow store — digest-addressed definition objects, the two-level coordinate index, .wnlp bundle install, fail-closed resolution (immutable defs; distinct from the runtime store) |
| src/engine.ts | the imperative shell: tick/green/reject/… → mutate → settle() |
| src/cli.ts | argv → engine calls, JSON on stdout |
Invariant: every engine mutation ends with settle() — materialize owed outputs and
run the cascade to a fixpoint — so status() is a pure read over artifact state and
never lies.
Storage
State lives in a single SQLite database via Node's built-in node:sqlite in WAL
mode — no native module to compile, no separate graph engine. The flat
artifact/task/run tables are the graph; the dependency structure is recomputed from
the definition on each tick. Concurrent advancement is made safe by a commit
fingerprint check: a run records the version of every input it claimed, and its commit
is rejected ("born-rejected") if any of those inputs moved underneath it. Each artifact
carries a monotonic version, so the engine can always ask "is this green output still
resting on the inputs it was built from?".
Retention and disposal. That file is the system of record and it keeps
everything: every artifact version's value and every run's issued order
packet — the dynamic input values it consumed and its rejection reasons,
including any sensitive values that flowed through an input — persist after a
workflow finishes. Authored prompt and command text are not in the packet
(orders are reference packets that carry a defDigest instead), so
instruction text is retained once, in the definition, not per firing. The default location is .owenloop/state.db (plus its WAL
-wal/-shm sidecar files), or wherever --db / OWENLOOP_DB / the db:
embed option points. Rotating or scrubbing that data is the operator's job:
owenloop delete <wf> removes an instance's rows, but SQLite frees those pages
without erasing or shrinking the file on disk, so for reliable disposal delete
the database file together with its -wal/-shm sidecars.
Testing
npm test # node --test, spec reporter
npm run typecheck # tsc --noEmit (type-checks the source)
npm run check # both
npm run build # compile src/ → dist/ (also runs automatically on npm pack/publish)The suite spans unit tests (paths, store, model, defs, schema,
util, cli), engine integration tests (the cascade, the stall, schema validation,
the concurrency check, judges: sign-off/CAS/throttling in test/judges.test.ts),
and end-to-end tests that spawn the real bin/owenloop.mjs binary and drive the
example workflows through their full lifecycles.
Two e2e files carry most of the weight, by opposite intent.
test/edge.e2e.test.ts is an edge battery aimed at the
corners the design is most particular about: cascade invalidation, terminal completion
surviving an upstream reject, empty / fully-retracted collections, the commit check,
cadence and daily-budget gating, the skip-cascade, and CLI robustness against malformed
input. test/scenarios.e2e.test.ts takes the opposite
tack — multi-step positive stories that confirm the documented behaviors hold
end to end: the map parallel cap, map and reduce firing as concurrent branches, the
reason thread riding the next job, stall → retry → re-stall, and the cascade re-firing on
a re-provided input while leaving a healthy graph and a terminal output untouched.
test/schema.e2e.test.ts drives schema validation end to end:
a malformed value is rejected rather than greened, a corrected value greens on the same
open job, repeated failures trip the stall and a retry clears it.
owenloop check <def> runs a bounded static reachability search over a workflow
definition and reports dead steps (never seen firing) split by severity: steps that
can NEVER fire regardless of search bounds are structurally dead — a real wiring
defect, reported nonzero exit — while steps that CAN fire but the bounded search
just didn't reach are unreached within bounds — informational only, exit 0.
It also splits every reachable non-done, no-moves state into exactly one of two
buckets: a stall state — the state is blocked only by a frozen/stalled debt
(maxAttempts / maxSchemaFailures / held) or an idle trigger waiting for its
threshold; recomputing eligibility with the freeze lifted (a human retry =
unlimited attempts) and with eventual idle time shows a move would become
available. This is an EXPECTED, by-design human-escalation brake or future wait
and never fails the check. The classifier does not add future idle transitions to
the timeless reachability search, so an idle-only completion path can still report
completable: false. A true deadlock is a state where the same recompute STILL
yields no moves, a genuine structural dead-end which fails the check nonzero when
the search is exhaustive. See docs/design.md §25
for the full breakdown.
By default, seedOwed inputs are assumed provided (modeling the operator's provide
already having run at create), so a def whose only initial gate is an unprovided seeded
input no longer reports a false True deadlocks ... (initial state) and nonzero exit.
--strict-inputs restores the seeded-inputs-start-owed behavior; when that's the sole
blocker, it also prints a one-line hint naming the seedOwed input(s) responsible.
--assume-provided is still accepted but is now a no-op (redundant with the default).
Design reference
owenloop is a faithful, decoupled implementation of a dataflow-engine spec.
docs/design.md is a self-contained walkthrough — the lifecycle,
firing rule, forward cascade, the reject kinds, the liveness rules, and the concurrency
model — cross-referenced from the source. docs/cli.md has the full
command reference, docs/authoring.md has the full YAML
grammar, docs/wire-contracts.md defines the
versioned trust-boundary records, and
docs/shift-logs.md is the on-disk log contract a shift
writes and an uploader reads.
License
Apache-2.0 © Typical Day LLC.
owenloop is permissively licensed — use, modify, self-host, and redistribute it, including in proprietary or closed-source products, under the terms of the Apache License 2.0.
Contributing
Contributions are welcome — see CONTRIBUTING.md. Note that owenloop requires every contributor to sign a Contributor License Agreement that assigns copyright in contributions to Typical Day LLC, so the project can be maintained — and relicensed in the future if ever needed — under one clear owner. The process is a one-time comment on your first pull request.
