Skip to content

feat(workflow)!: a rule is an effect and an exit; durations are strings - #62

Merged
Tielem merged 1 commit into
devfrom
feature/syntax-foundations
Sep 23, 2026
Merged

Tielem merged 1 commit into
devfrom
feature/syntax-foundations

Conversation

@Tielem

@Tielem Tielem commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Two breaking, mechanical changes from plans/in-progress/syntax-foundations.md, riding the unreleased 0.20.0. Neither adds behaviour; both exist so that the features that need them (suspend, endpoints) can be purely additive.

1. A rule is an effect and an exit

emit used to be two things at once: append a value, and go to activity X, which it carried inside itself as a required transition. That reads fine in onSuccess and nowhere else — a rule that runs while an activity is live has nowhere to continue to, so every such list would need a second, transition-less spelling of the same word.

Every rule, in every list, is now:

  • an optional condition;
  • at most one effect — emit, workflow.call;
  • at most one exit — transition, result, error.
# Before                              # After
- emit:                               - emit: { messages: "{{ output.messages }}" }
    value: { messages: "…" }            transition: { name: wait }
    transition: { name: wait }

The lists differ only in whether an exit is required: onSuccess and onFailure end the activity, so the run must go somewhere; an onEmitted rule runs once per value, and there an exit is what stops consuming.

  • handle becomes workflow.call. It does what the workflow.call activity does, so it takes that name, and the dotted key resolves through the effect oneof exactly as type: workflow.call resolves through config. Named for what it dispatches, since workspace.exec is already proposed and a bare call: would stop reading.
  • EmissionRule collapses into TransitionRule. One rule type, not two.
  • emit is allowed in onEmitted, which is what a filtering consumer needs — republish only the invoices, without a document whose only job is to republish a value.
  • At most one effect, enforced by the oneof, since two would need an order the document cannot express.
  • A rule with neither an effect nor an exit is UTOS-T001.

2. Durations are strings

timeout: 24h was never expressible: the source format ends by reading the document as proto3 JSON, where a duration is spelled only in seconds. The timer's own examples used 30s, which is why nobody noticed.

A duration is now a string in the unit shorthand that Kubernetes, Argo, Prometheus and Docker Compose use — 90s, 8h, 1h30m, 3d — whole numbers, largest unit first, no fractions and no ISO 8601. 86400s stays valid, so every timer written today keeps working.

It may also be a whole-field template, so a reusable document can let its caller choose how long it waits, and a backoff can finally honour a Retry-After header. The rendered value and the instant it resolves to are recorded when the activity is entered, so a replay reads the same deadline. duration joins the type registry as utos:duration.

Codes

  • reworded: UTOS-T001 (at most one effect, at most one exit, not neither), UTOS-C202 (a literal duration is positive)
  • new: UTOS-C203 (a literal duration's syntax), UTOS-E106 (what a duration template renders to)
  • retired: UTOS-T004, UTOS-C504

Decisions worth a look

  1. No spawn effect yet. The plan's proto sketch lists it, but it comes from spawn-action.md, still in preparing/, and it would be new behaviour. DispatchEffect has call only and field 2 is left free, so adding it later is additive.
  2. emit and result stay google.protobuf.Struct, not WorkflowMap — the one place this departs from the plan's sketch. A bundle holds templates, never values, and no template can contain a blob. The blob case this raises (an endpoint receiving a binary PUT) works through the template rendering to a value at run time; the two notes it does need are recorded in suspend.md.
  3. No "two exits" fixture, which the plan's verification list asks for: two members of one oneof cannot survive proto3 JSON parsing, so a validator never sees it.

Breaking

TransitionRule's action oneof is replaced by effect and exit; EmitAction and EmissionRule are gone; CallActivityConfig.on_emitted and TimerActivityConfig.duration take new field numbers, with the old ones reserved. buf breaking reports exactly these and nothing else.

Migration tables are in workflow-source-format.md § Migrating.

Checks

  • buf lint clean; buf breaking shows only the intended changes.
  • Six fixtures migrated to the new shape; new fixtures for the rule shapes, both duration codes, the dotted effect key and two effects in one rule. The evaluation corpus gains a duration form.
  • Every fixture parses, every link resolves, and the new expressions are syntax-checked.

sdk-dotnet, the daemon, the CLI and workflows/ follow; the plan tracks them.

Two breaking, mechanical changes that introduce no new behaviour, so the
features that need them stay additive.

A rule, in every list, is now an optional condition, at most one effect
(emit, workflow.call) and at most one exit (transition, result, error).
`emit` no longer carries the transition inside itself, which only ever
made sense in a list whose rules end the activity; `handle` becomes
`workflow.call`, named for what it dispatches, and EmissionRule collapses
into TransitionRule. An `emit` effect is allowed in onEmitted, which is
what a filtering consumer needs.

A timer's `duration` is a string in the unit shorthand (90s, 8h, 1h30m),
literal or a whole-field template — `24h` was never expressible before,
since proto3 JSON spells a Duration only in seconds. `duration` joins the
type registry as utos:duration.

Codes: UTOS-T001 reworded; C202 reworded and C203 added for a literal;
E106 for what a template renders to; T004 and C504 retired.

BREAKING CHANGE: TransitionRule's `action` oneof is replaced by `effect`
and `exit`; EmitAction and EmissionRule are gone; CallActivityConfig
.on_emitted and TimerActivityConfig.duration take new field numbers.
@Tielem
Tielem merged commit c6c9712 into dev Sep 23, 2026
2 checks passed
@Tielem
Tielem deleted the feature/syntax-foundations branch September 23, 2026 12:21
Tielem added a commit that referenced this pull request Sep 23, 2026
* feat(workflow): binary data — Blob and File values, WorkflowValue, BlobService (0.20.0) (#60)

A Blob is now a value: response.body, transitions, sub-workflows, results
and a run's input can all carry bytes. Small blobs ride inline; large ones
live in object storage behind a handle, and clients upload and download
directly at signed locations, multipart and resumable.

- workflow/v1/value.proto: WorkflowValue/WorkflowList/WorkflowMap/Blob.
  Run-time values on the daemon wire move off google.protobuf.Struct, so a
  type is structure and never content — no key is reserved.
- daemon/v1/blob.proto: BlobService, including SignUploadParts.
- docs: workflow-values.md and binary-data.md (new); await, async arrows
  and instanceof admitted; Blob/File in expressions; response.bodyText
  retired (UTOS-E070); blob and file in the type registry with mediaType
  and maxSize; UTOS-F101–F104, V101–V102, H015, S015.
- conformance: blob cases in the wire form, instanceof, schema and source
  cases for the published types.

BREAKING CHANGE: ExecutionPayload.data, GetExecutionResponse.result and
WatchOutputResponse.value/result change type; response.body is a Blob and
response.bodyText is retired.

* docs: end-to-end read of the spec — one owner per subject, drift corrected (#61)

Each document now owns one subject and the others refer to it:
template-expressions.md defines scope once (including what `output` holds
per activity kind and in an onEmitted rule); the source format keeps the
authoring guidance. The README maps the spec, indexes every rule-code
family and lists the corpora and who runs them.

Corrected, where the text had drifted from the protos or the validator:
- UTOS-B006 covers every document a bundle names (branches, handle)
- an onEmitted rule has four actions; EmissionRule's comment described
  the 0.0.13 flat dispatch
- source examples wrote `result:`, which the source format rejects
- the build step rewrites every document name and `self`
- `$ref` chains must terminate, not "acyclic"
- the content digest is provisional, not "leave it empty"
- CallActivityConfig.input, a v-prefixed example version, stale README
  entries (registry package, "planned" implementations)

History that had settled into normative text moved to Migrating sections.
No wire change: buf breaking against feature/binary-data is clean.

* feat(workflow)!: a rule is an effect and an exit; durations are strings (#62)

Two breaking, mechanical changes that introduce no new behaviour, so the
features that need them stay additive.

A rule, in every list, is now an optional condition, at most one effect
(emit, workflow.call) and at most one exit (transition, result, error).
`emit` no longer carries the transition inside itself, which only ever
made sense in a list whose rules end the activity; `handle` becomes
`workflow.call`, named for what it dispatches, and EmissionRule collapses
into TransitionRule. An `emit` effect is allowed in onEmitted, which is
what a filtering consumer needs.

A timer's `duration` is a string in the unit shorthand (90s, 8h, 1h30m),
literal or a whole-field template — `24h` was never expressible before,
since proto3 JSON spells a Duration only in seconds. `duration` joins the
type registry as utos:duration.

Codes: UTOS-T001 reworded; C202 reworded and C203 added for a literal;
E106 for what a template renders to; T004 and C504 retired.

BREAKING CHANGE: TransitionRule's `action` oneof is replaced by `effect`
and `exit`; EmitAction and EmissionRule are gone; CallActivityConfig
.on_emitted and TimerActivityConfig.duration take new field numbers.

* docs: second read of the spec — one vocabulary, and an author-first order (#63)

A rule has an effect and an exit since 0.20.0, but "action" survived in a
dozen places, including a build step that still rewrote an onEmitted
rule's `handle.workflow` — a key that no longer exists.

- workflow-source-format.md reads in the order it is used: the `type`
  discriminator's derivation and the normative mapping move into
  § Building a bundle, which says it is for implementers; the worked
  example comes before it; § Activities keeps one author-facing table of
  the kinds instead of two tables with different columns.
- The stream's guarantees (ordering, back-pressure, the relay,
  subscription lifetime) are stated only in execution-output-stream.md,
  which also names the handler where it defines it. The source format
  shows the syntax and points there.
- Each long document opens with a map of its sections.
- bundle.proto and workflow.proto: two more v-prefixed example versions,
  a description crediting the CLI with building bundles, and a note that
  named one of the three places a bundle names a document.

* docs: finalize v0.20.0 for release (#64)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant