Event-sourced
Each execution is an ordered log on its own subject. State, snapshots, the visibility index, jobs and timers are projections of it — and can always be rebuilt from it.
Packtrail interprets declarative flow graphs — task, choice,
fanout/join, await, map and
subflow nodes, in YAML or Go structs — on nothing but
NATS. Each execution is an ordered event log; state, indexes, jobs and timers are derived
from it, so time travel, forks, replays and audit come for free. Workers
are plain NATS consumers, in any language.
Four commitments shape every line of the engine.
Each execution is an ordered log on its own subject. State, snapshots, the visibility index, jobs and timers are projections of it — and can always be rebuilt from it.
You declare the graph; packtrail interprets it. No determinism rules for your code, no replay of your functions. Executions keep the flow version they started with.
A task is a job on <ns>.work.<kind>. A worker answers with one command over a small, versioned JSON protocol. The Go SDK is the reference.
One message per decision with an expected-sequence check for single-writer appends, message schedules for timers and cron, KV and object stores for the rest. Nothing else to run.
Per-attempt timeouts, fixed / linear / exponential backoff on durable timers, output JSON Schema, a result cache and per-key concurrency limits across executions.
Static branches joined with all, any or quorum:N; dynamic maps over a list computed at run time, with bounded parallelism and ordered results.
Tasks write deltas to channels folded by reducers — replace, append, merge, sum — so parallel branches never overwrite each other. Choices route on expr expressions.
await nodes wait for a signal with a mandatory timeout route; a worker can interrupt with a question and be resumed with the answer. Update writes state synchronously.
Call another flow as a child execution; let a worker pick its successor from a declared list; route permanent failures to a compensation node with on_failure.
Read the state right after any event, fork a new execution from the past (optionally editing its state), or rerun a node after a fix. Cron schedules and message triggers start flows.
The engine processes commands per partition: load state, decide, append the decision with optimistic concurrency, ack. The dispatcher turns stored events into effects — jobs, timers, child starts, index updates. Workers answer with commands. Nothing is decided twice.
work.<kind><ns>-events
stream · one message per decision, the source of truth
<ns>-cmd
stream · commands, timers, cron
<ns>-work
stream · jobs per worker kind
<ns>-index
KV · projection for List / Query
<ns>-snapshots
KV · fold snapshots every 100 events
YAML or Go structs, validated before anything runs: unreachable nodes, routes into a fan-out branch, bad expressions and unknown reducers are rejected up front.
name: review
channels:
notes: {reducer: append}
nodes:
- id: draft
type: task
kind: writer
timeout: 1m
retry: {max_attempts: 3, backoff: exponential}
next: check
- id: check
type: choice
rules:
- {when: "results.draft.score >= 8", to: approve}
- {default: true, to: draft}
- id: approve
type: await
signal: approval
timeout: 48h
next: publish
- {id: publish, type: task, kind: publisher}
start: draft
taskA job for a worker of kind; retries, timeout, cache, concurrency, output schema, dynamic edges. choiceFirst matching when rule wins; exactly one default. fanout · joinParallel task branches, closed by a join with all, any or quorum:N. awaitWait for a named signal; timeout is mandatory, on_timeout routes. mapOne task per element of over, at most max_parallel at a time. subflowRun another flow as a child; its output and counters flow back.Run them in one binary or in a hundred. The engine provisions its namespace on first run; workers share a durable consumer per kind; the client drives executions.
Because the log is the truth, any past point is a valid state. A stream sequence identifies a
decision; StateAt, Fork and Rerun cut between decisions.
The packtrail CLI exposes all of it.
packtrail-ui is a debugging dashboard and a plain client of the deployment: it
needs only NATS. It serves every namespace on the NATS account, switchable from the header.
Filter by status, flow or search attribute; quarantined executions are flagged.
Every node and route, including on_failure and dynamic edges.
Click an event to see channels, results and counters exactly as they were.
Signal, resume, cancel, fork, rerun, and redrive dead letters.
# NATS_URL is honoured (default nats://localhost:4222)
$ packtrail-ui -addr 127.0.0.1:8088 \
-namespaces orders,billing