Millstream vs Inngest and Trigger.dev
Inngest and Trigger.dev run durable background functions for applications: code triggered by events or schedules, split into steps that retry and resume on their own, offered first as hosted services. Millstream is a separate system that runs declarative pipelines of items through code, LLM agents and human review, self-hosted from a laptop to a server.
At a glance
| Millstream | Inngest, Trigger.dev | |
|---|---|---|
| Definition | pipeline.yaml: triggers, streams, steps |
Functions or tasks in your application's code |
| Languages | Any program speaking a JSON-lines protocol over stdio; Go SDK | TypeScript (Inngest also has Python and Go SDKs) |
| Unit of work | One delivery of one item to one step | One function run, with its steps |
| Waiting | Human review with timeout and escalation |
Durable sleeps and wait-for-event steps |
| Agents | Agent actions with schemas, sandbox, transcripts, budgets and evals | Steps that call a model, written by you |
| Lineage | Per item, across steps and pipelines | Run history per function |
| Hosting | Self-hosted only: one binary, SQLite or Postgres | Hosted service; self-hosting documented |
| Licence | Apache-2.0 core, commercial edition | Trigger.dev Apache-2.0; Inngest per its repository |
| Status | Pre-release: M1 to M5 built and tested | Released, widely used |
Where the models differ
Pipelines of items, beside the application
A function in Inngest or Trigger.dev lives in the application's codebase and its control flow is the function body. A Millstream pipeline lives in its own package and its control flow is the graph: steps read and write streams, and route objects send each output to the first stream whose CEL condition holds.
qualify:
action: lab/qualify-lead@1
in: leads.enriched
out:
- { when: "item.tier == 'hot'", out: leads.hot }
- { when: "true", out: leads.other }
An output that matches no route becomes an unrouted dead letter, so a gap is visible and replayable.
Data stays on your infrastructure
Millstream is single-tenant by design: one deployment per organisation, on its own machines. On a laptop, millstream run uses SQLite and needs no service. On a server, the same package runs on Postgres with a hot standby and remote nodes, and only the environment file changes. In the soak test, the standby took the lock within 29 to 30 ms of the active engine's death, and every run settled with no item published twice. Personal data has per-stream retention and millstream purge --key, which follows lineage into other pipelines.
Agents, review and cost as first-class steps
Agent steps declare their model, tools, network destinations, maxTurns and maxCostUsd; output is validated against a schema and every transcript is kept. budget.runUsd and budget.dayUsd hold back agent deliveries once spent, and pause the agent steps until someone resumes them. Review steps give people an inbox with editable fields and reason codes, and the intervention rate is a metric per step.
When to choose Inngest or Trigger.dev
- You are adding reliable background jobs to a TypeScript application and want them next to its code.
- Your flows need durable sleeps, waits on application events, or control flow decided at runtime. Millstream's graph is static, with no generic sleep or wait-for-event step.
- You prefer a hosted service to running an engine and a database yourself. Millstream is self-hosted only.
- You need a released, widely used tool today. Millstream's first release is the next milestone.
Using them together
An application function can hand work to Millstream through a webhook trigger: POST /hooks/<pipeline>/<trigger> with the trigger's token and an Idempotency-Key, so a retried step gets back the run it already fired with 200 instead of opening another. Millstream's results flow back through an action declared effects: external, or the stock webhook sink that comes with the first release.