design: one attempt-outcome model beneath every task — "what happened externally", "what result was obtained", "does it satisfy the contract", "is repeating safe" and "does policy and budget permit another attempt" are five answers that `ErrorKind`, `expect:`, the plugin's `retryable`/`unknown_outcome`/`retry_after` and `undo:` each give one of
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Written against b015dc6. This is the design record #1893 and its sibling (the parse_json: route) are instances of; it is filed so the fix for expect: is not the third task-specific patch on a shared problem.
Observed behavior
Every task answers one question about a failed attempt, and the engine treats that answer as all five:
ErrorKind.Retryable()(errors.go:143-150) is a closed set of four kinds. The kind is chosen by the task at the moment of failure and decides retry alone. Its own comment records the reason the default is permanent: "Retrying a POST that already took effect is worse than surfacing a failure that might have resolved on its own."http'sexpect:"replaces that judgement entirely" (eval_task_http.go:571-574): an unmet expectation isInvalidInputand permanent whatever the status said (#1893), and aparse_json: truedecode failure isInvalidInputbefore the status is read at all (eval_task_http_run.go:1078-1084).- A plugin reports
retryable,unknown_outcomeandretry_after(proto/flowstate/plugin/v1/plugin.proto:584-601). That is already three of the five answers, carried on a protocol the core model does not read back into a shared shape. undo:is "Registered on success, and only on success" (docs/DSL.md:3167-3176): a step that fails registers no compensation, including one whose external effect succeeded and whose postcondition then failed.ExecuteRequestcarries no run, step or attempt identity (#1677), and a retried attempt cannot learn what the previous one did (#1516).
The assessing pass ran five HTTP response-classification probes at b015dc6:
| Response and configuration | Classification | Retry advice |
|---|---|---|
| 503, default handling | Upstream |
retryable, Retry-After preserved |
503, unmet expect: |
InvalidInput |
permanent, delay lost |
429, unmet expect: |
InvalidInput |
permanent, delay lost |
503 HTML, parse_json: true, no expect: |
InvalidInput |
permanent, delay lost |
| 200, unmet postcondition | InvalidInput |
permanent |
Only the first row is right, and fixing expect: alone leaves the fourth.
Desired outcome
A typed attempt outcome beneath the syntax, defined once in Protobuf (invariant 1) and produced by every task, built-in or plugin:
- External effect: none, known, partial, or unknown. HTTP's non-idempotent rule (
eval_task_http_run.go:1023-1030) and the plugin'sunknown_outcomeboth populate this field rather than folding it into a kind. - Obtained result: the response or output, including a structured domain outcome where the task has one (the typed-outcome half is its own record).
- Contract satisfaction: what
expect:,must:on outputs, and a plugin's declaredoutput_message(#1476) decide. A false answer here is not evidence about 1 or 4. - Repeat safety: derived from 1 and the operation's declared idempotency (#1603's facts), never from 3. A general retry predicate must never turn a policy denial into permission or an uncertain write into a safe repeat; HTTP's own rule is that repeatability follows operation semantics or evidence the request was not applied (RFC 9110 §9.2.2).
- Permission and budget: the step's
retry:, the run's remaining budget (#1592), and policy.
ErrorKind becomes a projection of the outcome for the timeline and flow get, not the source. Retry scheduling (retry_after, backoff) is shared between HTTP and plugins; the reasoning about whether a write committed is not, and the model must not pretend otherwise.
Compensation follows: an effect recorded as known or partial before a contract failure must register the step's undo: (or a recovery record) rather than nothing. Recording an effect receipt cannot make the external operation and its durable record atomic, and the design must say what the gap between them means.
Acceptance criteria
- One Protobuf message (name open) carries the five answers;
httpand one mutating plugin (postgresis the candidate) produce it, and a conformance case asserts both drivers schedule the same retry from the same outcome. - The five probes above classify as: row 1 retryable; rows 2 and 3 retryable with the peer's delay; row 4 retryable; row 5 permanent.
- A step whose effect is known and whose
expect:is unmet has itsundo:registered, proven by a case that compensates it on run failure. ErrorKindis derived from the outcome by one function, andParseErrorKind(errors.go:365) still round-trips every kind.docs/DSL.mdstates the five questions once and pointsexpect:,retry:,undo:and the plugin contract at them.
Constraints and dependencies
- Instances: #1893 (
expect:fuses success and retryability), theparse_json:sibling filed beside it, #775 (polling has no spelling). - Adjacent: #1879 (a task is the unit of recovery), #1516 (a retried attempt learns nothing), #1677 (no attempt identity crosses to a plugin), #1476 (outputs never checked against
output_message), #1603 (idempotency as a fact), #1592 (run-wide budget). - Bounded: the outcome adds fields to an activity result already in history; no unbounded payload may enter through the "obtained result" field.
- Both drivers: the outcome is produced inside
Task.EvalInScope, which both drivers run.
Open questions
- Does the outcome live on
Node.Outputsor on the activity error, given Temporal carries the two on different paths? - Is a typed domain outcome (row 2) part of this message or the separate design for expected business outcomes as values?
- Which compatibility decision covers durable executions that recorded the old
ErrorKindstring before this lands?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with errors.go, eval_task_http.go, eval_task_http_run.go, and proto/flowstate/plugin/v1/plugin.proto to trace current classification and plugin retry fields. Read the five acceptance probes, then inspect Task.EvalInScope and docs/DSL.md. Done means HTTP and a mutating plugin produce the shared outcome, retry consistently, register undo after known effects, and preserve ErrorKind parsing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend-api-design, distributed-systems
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100