Workflow Engine: Temporal Gap Closure (Next Phase)
Document ID: WF-GAP-001
File Path: docs/03-workflow-engine/temporal-gap-analysis.md
Version: 1.1.0
Status: Substantially done — G1–G4, G6, and G7 are shipped (see the §3
status column, kept current), G5 (child workflows) shipped as a prototype
(ADR-0008); the v1.1 P3 scale-hardening
tickets (WFL-301..308) later extended the engine beyond this doc’s scope
Owner: Workflow Engine Team
Last Updated: 2026-07-15
1. Purpose
Section titled “1. Purpose”This document captures the gaps that matter between Wovyr’s wovyr-workflow
engine and Temporal, scoped as the durable-execution work for the next
development phase. It is deliberately narrow: it lists only the gaps that a
buyer evaluating Wovyr as a “durable execution layer for agents” would notice,
and that are worth closing given Wovyr’s positioning. General-purpose Temporal
features that conflict with Wovyr’s declarative DAG model (unbounded loops,
continue-as-new, arbitrary code replay) are explicitly out of scope — see
§6.
For the full competitive comparison and the rationale behind this scope, see the positioning note in §7.
2. Framing
Section titled “2. Framing”Wovyr does not aim to beat Temporal as a general-purpose durable-execution engine. Its differentiators are:
- Declarative authoring — a YAML DAG that can be validated, diffed, reviewed, and generated by an LLM safely (no replay-determinism footguns).
- Agent-native — the engine is wired into the gateway (
wovyr-provider), sandbox (wovyr-tools), and memory (wovyr-memory) in one binary. - Determinism by construction — no user code in the hot path, so no non-determinism sandbox is needed.
In that frame, the static-DAG constraints are acceptable trade-offs. The gaps below are the ones that remain genuinely valuable without abandoning that model.
3. Gaps In Scope (priority order)
Section titled “3. Gaps In Scope (priority order)”Effort is rough (S ≤ 1 wk, M ≈ 1–3 wk, L ≈ 1 mo+, per engineer). Impact is the perceived value to an evaluating buyer.
| # | Gap | Impact | Effort | Status |
|---|---|---|---|---|
| G1 | Durable wall-clock timers | High | M | Done |
| G2 | Schedules / cron | High | S–M | Done |
| G3 | Queries (read live state) | Med-High | S | Done |
| G4 | Visibility surface (list/inspect + minimal UI) | High | M–L | Done |
| G5 | Child / sub-workflows | Med | L | Prototype + ADR |
| G6 | Horizontal scaling story (honest tiering) | Med | M | Done |
| G7 | In-flight definition versioning | Med | M | Done (pinning) |
Implementation status (2026-06-29). G1, G2 (interval and cron), G3, G4, and G7 are implemented in
crates/wovyr-workflow(+wovyr-server,wovyr-cli) and exercised bytests/temporal_gaps.rs, thecron/scheduleunit tests, and the server’slists_and_inspects_workflow_executionstest (deterministic,ManualClock-driven). Cron is a dependency-free 5-field/@macroevaluator (UTC, Vixie DOM/DOW semantics) incron.rs. G4 visibility:CheckpointStore::list+Engine::list/history, server routesGET /api/v1/workflows(filter byworkflow/status/limit) andGET /api/v1/workflows/{id}(summary + event timeline) backed by a read-only engine over the durable store, a minimal read-only UI atGET /workflows, and CLIwovyr workflows list/show. CLI surface also:wovyr workflows status(G3),wovyr workflows tick, andwovyr workflows schedule create --every|--cron / list(G2). G5 child workflows are a prototype: the decision is recorded in ADR-0008 (child-as-activity over inline expansion) and the engine handles aworkflow-typed activity by running a child execution (derived id<parent>::<activity>, durable + visible via G3/G4), exposing its result to the parent — covered byparent_fans_out_to_two_children…and child-failure tests. G6 scaling ships queue partitioning (shard_of+PartitionAssignment+WorkQueue::lease_sharded, in-memory and Postgres, withWorker::with_partitions), a contention test (sharded_pools_lease_disjoint_executions), measured throughput baselines (tests/perf.rs), and a published scaling envelope (distributed-execution §33). All seven gaps are now addressed. Remaining follow-ups are non-gap polish: the G5 items in ADR-0008 §Consequences (concurrent children, input templating, cascading compensation, depth guard, CLI wiring) and G4 recency ordering / search attributes (current ordering is by execution id to stay deterministic). Per-gap “Approach”/“Acceptance” notes below are retained as the design record; the Done gaps now describe shipped behavior.
G1 — Durable wall-clock timers
Section titled “G1 — Durable wall-clock timers”Problem. Engine::fire_timer is caller-driven today: a wait activity on
{timer: <id>} suspends until something external injects timer.<id>. This keeps
the engine deterministic but means the engine cannot autonomously fire
“sleep 30 days, then escalate”. Durable timers are table-stakes for real
workflows (SLA escalation, reminders, delayed retries, billing cycles).
Approach. Introduce a durable timer queue alongside the event log: a
wait {timer} activity persists a fire_at wall-clock deadline as part of its
checkpoint. A timer-dispatcher (a role of the existing Worker) polls due timers
(WHERE fire_at <= now() in PostgresStore, FOR UPDATE SKIP LOCKED) and calls
the existing fire_timer path. Determinism is preserved: the injected event is
still what advances the workflow; only the trigger becomes time-based. The
fired timestamp is recorded in the event so replay/resume stays deterministic.
Acceptance.
- A workflow can declare
wait: { timer: { after: "30d" } }(and an absoluteat:form) and resume autonomously with no external signal. - Timer survives process restart (deadline persisted; another worker reclaims).
- Unit test with a virtual clock asserts deterministic firing order.
Touches. state.rs (timer field), store.rs + postgres.rs (due-timer
query), worker.rs (dispatcher role), definition.rs (DSL after/at).
G2 — Schedules / cron
Section titled “G2 — Schedules / cron”Problem. No way to start a workflow on a recurring schedule. Common, high-perceived-value, and cheap once G1’s timer infrastructure exists.
Approach. A Schedule record (cron or interval spec + workflow name +
default input) persisted in the store; the timer-dispatcher from G1 also evaluates
schedules and calls Engine::start at each due tick. Support overlap policy
(skip / allow / buffer-one) and a paused flag. CLI: workflows schedule create|list|pause|delete.
Acceptance.
- A cron schedule reliably starts executions across worker restarts.
- Overlap policy honored; missed ticks during downtime handled per a documented catch-up policy.
Touches. New schedule.rs, worker.rs (dispatcher), CLI.
G3 — Queries (read live state without resuming)
Section titled “G3 — Queries (read live state without resuming)”Problem. No way to read a running execution’s current variables / activity states without driving it. Operationally common (“where is order #123?”).
Approach. Cheap because state is already checkpointed. Add
Engine::query(execution_id) -> ExecutionSnapshot that reads the latest
checkpoint (variables, per-activity ActivityState, current waits) without
acquiring a lease or appending events. Expose over the server
(GET /api/v1/workflows/{id}) and CLI (workflows status).
Acceptance.
- Querying a running, suspended, completed, and failed execution returns correct state with no side effects (no new events, no lease contention).
Touches. engine.rs (read path), wovyr-server, CLI.
G4 — Visibility surface
Section titled “G4 — Visibility surface”Problem. No way to list/search/inspect executions; no UI. Temporal’s Web UI is one of its strongest adoption drivers (“I can see my workflows”).
Approach. Two layers:
- List/search API —
GET /api/v1/workflowswith filters (status, name, time range) backed by an indexed executions table inPostgresStore. Define a small set of indexable attributes now (status, name, started_at, updated_at); defer arbitrary search attributes. - Minimal read-only UI — execution list + a per-execution timeline view (the event log rendered as a DAG with activity states). Read-only first; the visual builder remains a v0.3 dashboard item.
Acceptance.
- Operators can list and filter executions and open a timeline showing each activity’s state, retries, and any compensation.
Touches. postgres.rs (executions index + queries), wovyr-server, dashboard.
G5 — Child / sub-workflows (prototype shipped — ADR-0008)
Section titled “G5 — Child / sub-workflows (prototype shipped — ADR-0008)”Problem. No composition: a workflow cannot invoke another as a unit. Needed once anyone builds non-trivial workflows. In tension with the static-DAG model — needs a design decision before committing.
Options to evaluate.
- (a) Activity-as-subworkflow — a
workflowactivity type whose executorstarts a child execution and suspends (via the existingInterrupted/wait primitive) until the child completes, then injects the child’s result as the activity output. Lowest-risk: reuses durable suspend/resume, keeps each DAG static. Recommended starting point. - (b) Inline expansion — compile a referenced sub-DAG into the parent at validation time. Simpler runtime, but loses independent lifecycle/retry and bloats history.
Acceptance (for the spike). A decision recorded as an ADR (section 17) with a prototype of option (a) running a parent that fans out to two children and aggregates results.
Touches. executor.rs, engine.rs, an ADR.
G6 — Horizontal scaling story (honest tiering)
Section titled “G6 — Horizontal scaling story (honest tiering)”Problem. Scaling today is a single WorkQueue with leased executions
(FOR UPDATE SKIP LOCKED in Postgres). Fine for moderate load; not Temporal’s
matching/history scale — and we should not pretend otherwise.
Approach. Rather than build a matching/history split (a multi-quarter effort that fights the positioning), document and harden the tier we have:
- Benchmark sustained executions/sec and lease throughput; publish the numbers.
- Add queue partitioning by a shard key so multiple worker pools don’t contend on one hot row range.
- Document the ceiling honestly and the migration path if/when a buyer needs more.
Acceptance. A published benchmark + a documented scaling envelope; partitioned queue passes a contention test with N worker pools.
Touches. queue.rs, postgres.rs, a perf test, docs.
G7 — In-flight definition versioning
Section titled “G7 — In-flight definition versioning”Problem. metadata.version exists but there’s no story for changing a
definition while executions of the old version are still running. Temporal solves
this with patching / worker versioning.
Approach. The declarative model makes the safe default easy: pin each
execution to the definition version it started with (persist the resolved
Definition — or its hash — in the execution record; Worker/DefinitionResolver
resolves that version on resume, not “latest”). New executions use the new
version. This covers the 90% case without a patching API. Document that
in-flight migration (rewiring a running execution onto a new DAG) is out of
scope.
Acceptance. Changing a definition does not break or alter executions already in flight; a resumed old execution runs its original DAG.
Touches. worker.rs (DefinitionResolver contract), store.rs /
postgres.rs (persist version/hash with the execution).
4. Suggested Sequencing
Section titled “4. Suggested Sequencing”- G1 (durable timers) first — it unblocks G2 (schedules) and is the single highest-value gap.
- G3 (queries) + G4 visibility API — cheap, and G4’s list view needs the read paths from G3.
- G7 (version pinning) — small, removes a correctness footgun before more workflows exist in the wild.
- G4 UI + G6 (scaling/benchmark) — parallelizable.
- G5 (child workflows) — spike + ADR; schedule build after the decision.
5. Cross-Cutting Acceptance
Section titled “5. Cross-Cutting Acceptance”- Every gap closed ships with tests (determinism preserved; the durability invariants in checkpointing-specification.md hold across restart).
- No new ambient clock/randomness in core logic — time-driven features (G1/G2) isolate the clock at the dispatcher boundary and record fired timestamps in the event log so replay stays deterministic (coding-standards §7).
- The README status, this doc’s own status table, and the roadmap exit criteria are updated as each gap lands.
6. Explicitly Not Doing
Section titled “6. Explicitly Not Doing”These Temporal features are out of scope because they conflict with the declarative DAG model or the positioning, not because they were overlooked:
- Unbounded/dynamic loops — DAGs are acyclic by validation (definition.rs).
- Continue-as-new — DAGs are bounded; no unbounded histories to truncate.
- Arbitrary code replay + non-determinism sandbox — there is no user code in the hot path by design.
- Updates (synchronous mutate-and-return) — revisit only if a concrete use case appears.
- Multi-language SDKs — authoring is declarative YAML; language SDKs are a separate, later bet.
7. Related
Section titled “7. Related”03-workflow-engine/overview.md·execution-model.md·checkpointing-specification.md·distributed-execution.md·compensation-engine.md18-roadmap/v0.3.md(dashboard / multi-tenancy)- Implementation:
crates/wovyr-workflow
8. Revision History
Section titled “8. Revision History”| Version | Date | Description |
|---|---|---|
| 1.1.0 | 2026-07-15 | Header status Planned → Substantially done, matching the long-current §3 table (G1–G4/G6/G7 Done, G5 prototype); no gap content changed |
| 1.0.0 | 2026-06-29 | Initial Temporal gap-closure scope for the next phase |