Skip to content

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


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.


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.


Effort is rough (S ≤ 1 wk, M ≈ 1–3 wk, L ≈ 1 mo+, per engineer). Impact is the perceived value to an evaluating buyer.

#GapImpactEffortStatus
G1Durable wall-clock timersHighMDone
G2Schedules / cronHighS–MDone
G3Queries (read live state)Med-HighSDone
G4Visibility surface (list/inspect + minimal UI)HighM–LDone
G5Child / sub-workflowsMedLPrototype + ADR
G6Horizontal scaling story (honest tiering)MedMDone
G7In-flight definition versioningMedMDone (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 by tests/temporal_gaps.rs, the cron/schedule unit tests, and the server’s lists_and_inspects_workflow_executions test (deterministic, ManualClock-driven). Cron is a dependency-free 5-field/@macro evaluator (UTC, Vixie DOM/DOW semantics) in cron.rs. G4 visibility: CheckpointStore::list + Engine::list/history, server routes GET /api/v1/workflows (filter by workflow/status/limit) and GET /api/v1/workflows/{id} (summary + event timeline) backed by a read-only engine over the durable store, a minimal read-only UI at GET /workflows, and CLI wovyr workflows list/show. CLI surface also: wovyr workflows status (G3), wovyr workflows tick, and wovyr 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 a workflow-typed activity by running a child execution (derived id <parent>::<activity>, durable + visible via G3/G4), exposing its result to the parent — covered by parent_fans_out_to_two_children… and child-failure tests. G6 scaling ships queue partitioning (shard_of + PartitionAssignment + WorkQueue::lease_sharded, in-memory and Postgres, with Worker::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.


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 absolute at: 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).


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.


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:

  1. List/search APIGET /api/v1/workflows with filters (status, name, time range) backed by an indexed executions table in PostgresStore. Define a small set of indexable attributes now (status, name, started_at, updated_at); defer arbitrary search attributes.
  2. 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.


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 workflow activity type whose executor starts a child execution and suspends (via the existing Interrupted/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.


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).


  1. G1 (durable timers) first — it unblocks G2 (schedules) and is the single highest-value gap.
  2. G3 (queries) + G4 visibility API — cheap, and G4’s list view needs the read paths from G3.
  3. G7 (version pinning) — small, removes a correctness footgun before more workflows exist in the wild.
  4. G4 UI + G6 (scaling/benchmark) — parallelizable.
  5. G5 (child workflows) — spike + ADR; schedule build after the decision.

  • 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.

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.


VersionDateDescription
1.1.02026-07-15Header status Planned → Substantially done, matching the long-current §3 table (G1–G4/G6/G7 Done, G5 prototype); no gap content changed
1.0.02026-06-29Initial Temporal gap-closure scope for the next phase