picatz / picatz/flowstate

Temporal's 79 Go samples mapped against Flowstate: what we can express, what's awkward, what's missing (design record)

Open
#272 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

design engine kind/design-record
Dominant language
Go
Stars
9
Forks
0
Avg merge
3h 3m
Merged PRs (30d)
509

Description

temporalio/samples-go is the closest thing to a canonical catalogue of what durable execution is actually used for. All 79 were mapped against Flowstate, verified by writing 23 probe Flowfiles and running flow validate / flow run local rather than by reading. Every refusal quoted below is real output.

Excluded: 6 Nexus samples (deliberately deferred), 5 pure-integration (datadog, zapadapter, …), and ~14 that are deployment shape rather than DSL shape.

The headline: Flowstate covers more than expected

sagaundo:, cron/scheduletriggers:, choice-*if:, branch/splitmerge/goroutinefor_each/parallel:, pollingloop:, child-workflowcall:, retryactivityretry:, timersleep:/wait_until:, await-signalswait_for_signal:, cancellation→compensating cancel, worker-versioning and worker-specific-task-queues→shipped, psoloop:+call:+transparent CAN.

dynamic/dynamic-workflows are refused on purpose (unknown key "${vars.which}") — the DSL constitution's "a reader tells data from code without consulting a registry". That is a decision, not a gap, and listing it either way would misrepresent it.

Verified gaps

Missing Evidence
run.self — a run cannot learn its own id references unknown field "id" of run; run has identity and local
Expression-valued durations sleep "${inputs.d}" is not a duration; write it as 30s, 5m, 1h, or 7dwait_until: takes an expression, sleep:/timeout: do not
A race / first-wins join runParallel drains every branch before inspecting errors (execute.go:1097)
Update (synchronous request returning a value) SignalResponse is {}; nothing in the service replies to a sender
A run-to-run signal task TaskNames() is exactly {http, log}
Per-step routing ARCHITECTURE.md says so in those words
Payload codec seam Filed separately as #271 — the doc names a mechanism the tree lacks

The mutex sample is the one that shows how sharp the run.self gap is. It is writable today — by calling Flowstate's own control-plane over http:, hand-writing protojson for Node.Outputs, and string-concatenating flowstate-entity-<ns>_<key>, reimplementing the private composition in entity.go whose unforgeability that file argues for at length. That spelling validates. Nobody should ever ship it, which is an argument for closing the gap rather than documenting the workaround.

Ranked plan — by unlock per unit of coherence risk

1. run.self + a flowstate.signal task. run is already a root whose doc argues additions are free; the signal goes through the front door (same RPC, same tenancy authorization) per #162's own rule. Composes with wait_for_signal:, entity_key, undo:, for_each. Unlocks expense, mutex, reqrespactivity, half of recovery, #166's supervisor shapes — and retires the cursed spelling above. Risk: low. Expose only workflow_id/run_idnot start_time, which is a clock read by another name, and now is bound only inside a wait for exactly that reason.

2. Expression-valued durations. Removes an inconsistency rather than adding a concept: route sleep: and wait_for_signal.timeout: through exprValue as wait_until: already is. Cheapest item here and a blocker in four samples. Risk: very low — but flow fix must learn now is bound in one more position, and that is precisely the class that has corrupted files twice.

3. race: — a first-wins branch group. Reuses Parallel.Branch verbatim. "Wait for all" and "wait for first" are the two joins and we have one. Unlocks pickfirst, updatabletimer (race a wait_until: against a signal), hedged requests, timeout-with-fallback. Risk: moderate, and stated precisely — loser cancellation needs a contract for what a cancelled-because-a-sibling-won step is; keep undo:-in-concurrent refused for race: too; and "which branch won" is decided by the world, so it must be written down as an explicit exception to invariant 3 in the same voice ARCHITECTURE.md uses for the local-signal exception, not discovered by someone whose local rehearsal picked the other branch.

4. Update. Unlocks seven samples — more than anything else — but it adds an interaction mode, not a composable primitive: authorization, signals: policy, MCP, flow test and the CLI all need an answer. Proposed shape keeps it a Wait arm so accept: is an ordinary CEL predicate in workflow code. Risk: high. reply: computed from existing scope is safe; "update writes a var" is not, and the temptation will be immediate. Should not start before signal authorization has settled.

5. Per-step runs_on:. The tenant-queue mechanism one level down; the unforgeable-separator argument transfers unchanged from #270. Also the natural home for per-step priority and rate limits, and it fixes plugins needing a bespoke worker each. Risk: low-moderate — reserve the key in futureStepKeys before building, and the validator must accept any well-formed fleet name without checking a fleet exists (deployment configuration an author's editor cannot see).

Below the line deliberately: undo: inside for_each/parallel (lowest risk of all — ARCHITECTURE.md already names the one missing piece — but no sample maps to it directly), and the workspace substrate #200, which is the largest unlock and the one not to start next: fileprocessing/session-failure are exactly the trigger #200 says to wait for, but it wants a store, a GC, a tenancy story and four bounds, and all five above are cheaper.

One correction to #200: worker sessions and #200 are the same problem from two sides — #200 asks how steps hand each other bytes, sessions ask how steps land on the same process. #200's content-addressed-blob shape dissolves the session requirement (if bytes are addressable, step 2 need not care which worker ran step 1, and that survives the worker dying). Its workspace: shape is closer to a session and inherits the fragility. What sessions need and #200 does not mention is heartbeat-driven session failure.

Features shipped with no example

Reachable, documented, but nothing in examples/ keeps them honest: per-tenant worker routing (#270), worker deployment versioning, call: inside for_each (verified working — the most-requested composition shape, and only loop:+call: has an example), the interrupt-driven forever-loop (sleep-for-days: loop: + long-timeout: wait_for_signal, where timing out means "do the periodic work"), and paged fan-out (loop: paging containing for_each).

Where Flowstate is better, honestly

Compared directly against samples-go/dsl/, Temporal's own YAML DSL: map[string]string bindings so nothing composes, positional arguments over a flat namespace, no conditionals, no loops, no per-element retry/timeout/error handling, no compensation, no waits, no validation at all (a typo is a runtime failure on a worker), unknown keys silently ignored, and the schema hand-written beside the interpreter so the two drift.

Across the corpus: the samples ship a workflowcheck static analyzer because determinism is per-engineer discipline — Flowstate's bet makes that file unnecessary. saga maintains a compensation stack by hand; undo: resolves at the moment the step succeeded. All three polling variants are unbounded; loop: has a mandatory ceiling that fails distinctly. cron's own README tells you to use Schedules instead. worker-versioning is a tutorial in operating build IDs; Flowstate refuses to start a worker given half the pair. expense hands an external system an opaque completion token — a capability to complete your activity — where Flowstate's answer is an authenticated, tenancy-checked signal. safe_message_handler is an extended lesson in not corrupting state with interleaved handlers; Flowstate's entity loop makes the bug class non-existent.

Two things the sample does better, so the comparison cuts both ways: its Parallel.execute cancels siblings on the first branch error where runParallel drains all of them, and result: <name> rebinds an activity's output to any variable name where Flowstate's output names are fixed by the task.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

This is a broad design record rather than a single implementation task. Start by reading ARCHITECTURE.md, execute.go around line 1097, and entity.go, then compare the ranked proposals and their cited issue dependencies. A contribution would need to narrow one proposal to a concrete scope, with its contract, affected entry points, tests, and acceptance criteria documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend-api-design, distributed-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.