steerium
steerium is an open-source, local-first TypeScript workflow orchestrator for deterministic code, AI calls, and coding agents, with recorded, replayable runs.
v0.5.0 · open source (MIT) · runs on your machine
Write the workflow.Not the runner.
steerium gives you the primitives — triggers, steps, providers — and a local daemon that keeps them live, records every run, and can replay it. What goes inside the function is entirely yours: deterministic code, a coding agent, or both in the same run.
npm install -g steerium Needs Node 22.13+
import { defineWorkflow, github } from "steerium";
export default defineWorkflow({ name: "review", on: github.prOpened({ repo: "acme/app" }), async run(ctx) { const { pr } = ctx.event;
const review = await ctx.step("review", () => ctx.agent.run({ provider: "claude", // the CLI you already log into allowedTools: ["Read", "Grep", "Bash"], prompt: `Review PR #${pr.number} against ${pr.baseBranch}. Correctness bugs only — skip style nits.`, }), );
const { token } = ctx.connector<{ token: string }>("github"); await github.comment(token, pr.repo, pr.number, review.text); },});01 Ideas
Nine ideas, to show the shape
None of these are features — they’re just workflows, each a single file someone wrote in .steerium/workflows/. Some call an agent, some are only git and a schedule. Yours will be different, and that’s the point.
github.prOpened()
Review every PR as it opens
An agent reads the diff inside the real checkout — not a pasted patch — and posts a review comment.
linear.ticketMoved()
Turn a ticket into a branch
Plan with a cheap API call, then let a coding agent take a first pass and push it for you to judge.
schedule.cron("0 16 * * 5")
Write Friday’s changelog
Read the week’s commits, draft release notes, and save them as an artifact you can actually diff.
schedule.every(6h)
Prune merged branches
No AI anywhere in this one. It’s git fetch --prune and a loop — automation doesn’t have to mean agent.
github.issueOpened()
Triage new issues
Summarize it, guess a label, and ask the reporter for the repro steps they left out.
approvals.responded()
Draft it, but ask me first
The agent drafts and stops. You reply “approve” and a second workflow commits and pushes.
schedule.cron("0 9 * * 1")
Bump deps, but only if green
Update, run the suite, open a PR when it passes and a note to yourself when it doesn’t.
github.prOpened()
Keep the docs honest
When a PR touches the public API, have an agent check whether the docs still describe reality.
schedule.cron("0 8 * * *")
Yesterday, summarized
Commits landed, tickets closed, and PRs still waiting on you — as a digest before you open your laptop.
defineTrigger() · defineProvider() · defineConnector()
…and whatever yours actually is
If the trigger you need isn’t in the box, write it — a queue, a file watcher, an internal webhook, a device on your network. Same for providers and connectors: the built-ins are written against the public API you get, so there’s nothing to fork and nothing to wait for.
See how the built-ins are built02 The code
Four real workflows, start to finish
No DSL, no YAML, no visual builder. These are copy-pasteable files from the examples directory.
A pull request opens. The agent reviews the diff from inside the checkout and comments back. Read-only tools — it can look, not touch.
import { defineWorkflow, github } from "steerium";
export default defineWorkflow({ name: "pr-review", on: github.prOpened({ repo: "acme/app", intervalMs: 60_000 }), timeoutMs: 15 * 60_000, async run(ctx) { const { pr } = ctx.event;
await ctx.step("fetch", () => exec("git", ["fetch", "origin", pr.branch]));
const review = await ctx.step("review", () => ctx.agent.run({ provider: "claude", allowedTools: ["Read", "Grep", "Bash"], prompt: `Review PR #${pr.number} ("${pr.title}") on ${pr.branch}. Diff against ${pr.baseBranch}. Correctness bugs and risky changes only — skip style nits.`, }), );
const { token } = ctx.connector<{ token: string }>("github"); await ctx.step("comment", () => github.comment(token, pr.repo, pr.number, review.text), );
await ctx.artifact.writeText("review.md", review.text); },});A ticket moves to Todo. A cheap model plans, a coding agent implements on a branch, and the ticket gets told what happened. concurrency: 1 keeps one agent in the repo at a time — the rest queue.
import { defineWorkflow, linear } from "steerium";
export default defineWorkflow({ name: "ticket-agent", on: linear.ticketMoved({ to: "Todo", intervalMs: 60_000 }), concurrency: 1, // one agent in the repo at a time timeoutMs: 45 * 60_000, async run(ctx) { const { ticket } = ctx.event; const branch = `steerium/${ticket.identifier.toLowerCase()}`;
const plan = await ctx.step("plan", () => ctx.agent.run({ provider: "openai", // cheap model for the thinking prompt: `Draft an implementation plan for ${ticket.identifier}: ${ticket.title}\n\n${ticket.description}`, }), );
await ctx.step("branch", () => exec("git", ["checkout", "-B", branch]));
await ctx.step("implement", () => ctx.agent.run({ provider: "codex", // real coding agent, chosen per call prompt: `Implement this plan, then run the tests:\n\n${plan.text}`, }), );
const { apiKey } = ctx.connector<{ apiKey: string }>("linear"); await linear.comment(apiKey, ticket.id, `Pushed to \`${branch}\`.`); },});Not everything needs a model. This one is plain Node and git on a six-hour interval — but it still gets the run history, the logs, and the replay.
import { defineWorkflow, schedule } from "steerium";import { execFile } from "node:child_process";import { promisify } from "node:util";
const exec = promisify(execFile);
export default defineWorkflow({ name: "repo-housekeeping", on: schedule.every(6 * 60 * 60_000), async run(ctx) { const pruned = await ctx.step("prune-merged-branches", async () => { await exec("git", ["fetch", "--prune"], { cwd: ctx.scope.cwd }); const { stdout } = await exec("git", ["branch", "--merged", "main"], { cwd: ctx.scope.cwd, }); const branches = stdout .split("\n") .map((b) => b.trim()) .filter((b) => b && !b.startsWith("*") && b !== "main");
for (const b of branches) { await exec("git", ["branch", "-d", b], { cwd: ctx.scope.cwd }); } return branches; });
await ctx.artifact.writeJSON("pruned.json", pruned); ctx.logger.info(`pruned ${pruned.length} merged branch(es)`); },});Waiting for a human isn’t a paused process. approvals.request posts the question and the run ends. Your reply is a new event that fires a second workflow — so waiting survives restarts, sleep, and reboots.
import { defineWorkflow, approvals, isApprove } from "steerium";
export default defineWorkflow({ name: "blog-approve", on: approvals.responded(), async run(ctx) { const { approval, reply } = ctx.event; const { draft } = approval.payload;
if (isApprove(reply.text)) { await ctx.step("publish", () => commitAndPush(draft)); await approvals.resolve(ctx, approval.id); return; }
// Anything that isn't approval is treated as feedback: revise and re-ask. const revised = await ctx.step("revise", () => ctx.agent.run({ provider: "anthropic", prompt: `Revise this draft.\n\nFeedback: ${reply.text}\n\n${draft}`, }), );
await approvals.reask(ctx, { id: approval.id, text: revised.text }); },});03 Mix
Deterministic and agentic, in one function
There is no “AI workflow” mode. ctx.step() wraps a shell command and a coding agent exactly the same way, so a workflow can be plain code, entirely agent-driven, or move between the two as many times as it needs.
Start deterministic. Most automations begin as git, fs, and an npm package. Nothing about steerium asks you to add a model.
Reach for an agent at the step that needs judgement. Summarizing, drafting, deciding, writing code — a call, not a rewrite.
Both land in the same record. Step status, output, logs, artifacts, and replay work identically whichever kind of step produced them.
- codecollectgit log --since=1.week --oneline
- agentdraftanthropic · group 42 commits into notes
- codewriteprepend CHANGELOG.md, commit
- agentopen-prclaude · push the branch, summarize it
one workflow · one run record · one replay
04 Scope
Global or in the repo. Same runner.
Some automation belongs to you, some belongs to a codebase. steerium runs both from one daemon, with one API and one run history — the only difference is where the file lives and what cwd it runs with.
Global
~/.steerium/workflows/
runs with
cwd = ~/.steerium- automation that isn’t about one repo
- digests, drafting, cross-repo chores
travels with
steerium config export
Project
<repo>/.steerium/workflows/
runs with
cwd = the repo root- committed alongside the code it automates
arrives with
git clone, reviewed in PRs- what makes agent workflows natural
one steerium daemon
Project config merges over global, and project wins. Run steerium start inside a repo that has a .steerium/ and the daemon scopes itself to that project automatically — no registration, works in a fresh clone.
05 Model
Three primitives — and you can replace all of them
The built-ins hold no privileged access. Every one of them is written against the same public API you get, so anything shipped is a worked example rather than a wall.
Emits events.
- cron & intervals
- GitHub, Linear, Jira
- webhooks (HMAC-verified)
- approval replies
- manual fires
defineTrigger — implement start(ctx, emit) and
you get persistent state for dedup cursors and webhook registration for
free.
Handles one event.
- an async function
- steps, logs, artifacts
- any npm package
- shell, git, fs
- yours
defineWorkflow — there is nothing to extend here, because
there was never a DSL in the way.
Runs an AI or agent call.
openai,anthropicclaude,codexmock(the default)- chosen per call
- bring your own
defineProvider — a local model, an internal inference
service, a different agent. Register it in config; workflows just name it.
1 an event arrives
2 deduped and persisted
3 your function runs, step by step
4 the run is recorded,
okorerror5 replay it later against the exact same event
No automatic retries, deliberately: agents commit code, push branches, and
leave comments, so a blind retry duplicates all of it.
steerium replay <runId> is the explicit re-run.
And you don’t have to write any of it by hand.
A workflow is a plain TypeScript file with a typed API and no DSL to learn, which makes it the kind of thing a coding agent is genuinely good at producing. Point Claude Code or Codex at your repo, describe the automation in a sentence, and let it write the file, fire it once against the mock provider, and read the run record to check its own work.
06 Start
Running in four commands
The default provider is mock — deterministic, offline, no key. You can watch the whole loop work before pointing it at a real model.
Once the daemon is up, 127.0.0.1:4319 serves a small browser UI: run history streaming live, per-step logs, artifacts, cancel, replay.
$ steerium init # scaffold ~/.steerium$ steerium workflow run hello # fire one workflow, right now$ steerium logs # every run is on the record$ steerium start # daemon: triggers, API, browser UIBuild something that runs itself.
Install it, scaffold a workflow, and have something of your own running in the next five minutes.