Flow
Workflows

Workflow specifications and metadata

Describe intended business behavior and connect optional workflow identifiers to execution records.

A workflow specification describes a business outcome and the steps needed to reach it. Optional metadata connects actions to that specification. It does not execute steps, enforce rules, or prove completion. Tracing continues to work without it.

The shared infrastructure is available. No business workflow is tagged yet. Lease activation and its initial charge schedule will be a separate pilot.

Write a specification

Copy the template:

frontend/docs-app/workflow-template.mdx

Save each definition in frontend/docs-app/content/docs/platform/workflows/definitions/<workflow_id>/<version>.mdx. For example, leases.activation/1.mdx would define leases.activation, version 1. That example is a path convention, not an existing specification.

Use a stable dot-separated lowercase workflow ID, a quoted positive integer version, and lowercase step IDs with underscores. The owner is the team responsible for the business rules. Status is draft, active, or retired. Each version is a complete specification. Preserve versions already referenced by recorded actions. Changes to steps, conditions, calculations, or completion rules require a new version. Typo fixes and changes to owner or lifecycle status can stay in the same version.

The template contains structured metadata followed by plain-language sections:

ContentWhat the author must explain
Purpose and scopeBusiness outcome, start and end, and exclusions.
Trigger and identityWhat starts one instance and how retries recover the same identity.
PrerequisitesRequired facts and their authoritative sources.
StepsStable IDs, dependencies, applicability, inputs, result, evidence, rules, timing, and exceptions.
Business rulesConditions that must hold across steps, including calculations and duplicate prevention.
TimingDeadlines and the events that start them, including calendar and timezone rules.
Exceptions and correctionsAllowed departures, approval, reasons, and correction of committed effects.
Completion and cancellationObservable evidence for the overall outcome.
ExamplesConcrete inputs and expected outcomes, including retries and conditional steps.
Open questionsDecisions that are missing; explicitly say none when resolved.
Implementation coverageAutomated, manual, and unobservable steps, and differences from intended behavior.

The portal renders the identity and step table from frontmatter. Do not maintain a second copy of that table in the prose. Each step has a ## Step: step_id section; its stable link is #step-step_id.

depends_on describes prerequisites, not an instruction to a scheduler. A condition such as "The final balance requires a refund" belongs in applies_when. State what happens when a dependency does not apply. Distinguish required-but-missing, not-applicable, and not-yet-determined. Repeated work uses the same step ID and a separate action ID for each logical item. A successful span is not proof that all expected items were committed.

Validate and find specifications

Run from the docs app directory so its pinned pnpm version is used:

cd frontend/docs-app
pnpm workflows:check
pnpm workflows:test
pnpm workflows:lookup leases.activation 1 record_charges

The lookup command is an example for a future definition; it reports an error until that definition and step exist. With an existing ID it prints the source file and portal URL. Published specifications use /docs/platform/workflows/definitions/<workflow_id>/<version>.

Validation checks identity, file paths, required fields and sections, duplicate step IDs, missing dependencies, cycles, and matching step sections. It runs in pnpm check and pnpm build, including the existing Docker build. It validates structure, not whether the business rules are correct or historical versions were edited. The copyable template is checked but is not registered as a business workflow.

Metadata contract

Outbox headers accept an optional object:

{
  "workflow": {
    "definition_id": "example.workflow",
    "definition_version": "1",
    "instance_id": "01931f00-0000-7000-8000-000000000001",
    "step_id": "record_result",
    "action_id": "01931f00-0000-7000-8000-000000000002"
  }
}
FieldMeaning
definition_idStable specification ID, at most 128 characters.
definition_versionVersion selected for this instance, a positive integer string of at most nine digits.
instance_idUUID identifying one execution of the business workflow, independent of trace IDs.
step_idSpecification step, at most 64 characters.
action_idUUID identifying one logical action, stable across retries.
parent_action_idOptional UUID of the action that caused this action; omitted or null for a root.

UUIDs use canonical lowercase form and cannot be zero. New identities should use UUIDv7, following platform conventions. The package does not allocate them. Only IDs belong here: no names, amounts, addresses, or other business data.

Attach and carry an association

Business code attaches an identity that it has already established durably:

ctx = workflow.With(ctx, workflow.Metadata{
    DefinitionID:      "example.workflow",
    DefinitionVersion: "1",
    InstanceID:        savedInstanceID.String(),
    StepID:            "record_result",
    ActionID:          savedActionID.String(),
    ParentActionID:    incomingActionID.String(),
})

Import common/workflow. Call Metadata.Valid() when checking an identity assembled by business code. With stores a value copy and clears the association if the value is invalid; it never fails the business operation. Use FromContext to read it.

The existing shared code carries valid metadata automatically:

  • envelope.MarshalHeadersFromContext writes it with the event in the outbox transaction.
  • Publisher adapters pass headers.Workflow as transport.Message.Workflow. The transport restores that saved identity, independently for each message in a batch.
  • Service Bus carries the workflow object in the unchanged envelope body. W3C trace context still travels in broker properties; workflow metadata is not duplicated there.
  • The shared consumer restores the envelope's workflow into the handler's context.
  • httpapi.NewClient sends the context in one Flow-Workflow JSON header. Shared HTTP middleware reads it. Use this client for internal destinations only.
  • Shared HTTP and messaging spans get workflow.* attributes. Contextual logs get the same fields at the root, even when a logger uses groups and tracing is disabled.

workflow.With does not relabel an existing span. An HTTP request or consumer span keeps the association it received. If business code selects a new action inside that operation, subsequent logs, event headers, and outgoing spans use the new association. For a separate custom span, use trace.WithAttributes(workflow.Attributes(ctx)...). Metadata.SpecificationURL() provides its conventional portal step URL without I/O; use the authoring lookup to check whether the document and step actually exist.

Passing a context continues an action; it does not automatically start the next business step. When a handler performs a different logical action, it must select the new step, assign or recover that action's ID, and set the incoming action as its parent. Several events may describe one action and share an action ID. Broker redelivery, republishing, and HTTP retries do not themselves create business actions.

Durability and boundaries

An outbox event retains metadata because it is committed in the existing JSON headers. No schema migration is needed for that path. A later independent HTTP request or a scheduled job has no inherited context: its owning service must save and recover the association from appropriate domain or command state. This shared package adds no workflow-instance table and does not recover associations from sampled traces. The pilot will implement the necessary domain persistence and retry behavior.

Metadata is descriptive and untrusted. An HTTP client or message producer can supply it. Never use it to authorize an operation, select business behavior, establish that a step completed, or trust an arbitrary workflow association. A domain that needs an authoritative association must recover it from its own state. Ingress accepts it only as correlation, just as it accepts trace context.

Malformed, oversized (over 2 KiB), or incomplete workflow objects are ignored. Unknown object fields are ignored for additive compatibility. Duplicate HTTP header values are ignored. Invalid workflow metadata does not discard otherwise valid request or trace identifiers and never stops publication or consumption. Missing metadata clears any association inherited from the polling loop.

Remove an association

Stop attaching metadata in the domain implementation, or explicitly use:

ctx = workflow.Without(ctx)

Pass that context to later work. This removes only the workflow association; tracing, request IDs, cancellation, and business behavior continue. The internal HTTP client also removes a stale Flow-Workflow header when the context has no association. Removing a child's association does not mutate an earlier context or an existing span.

Committed events and historical spans retain their original metadata. Already queued messages can still carry it. Retain the referenced specification versions; correct business rules in a new version rather than rewriting their historical meaning.

Test locally

These tests require no database, broker, Jaeger, or Datadog account:

go test -race -count=1 ./common/workflow ./common/envelope ./common/httpapi ./common/transport ./common/logging

They exercise HTTP, serialized outbox headers, publication and consumer contexts, follow-on events and HTTP, retry identities, contextual logging, and removal. The same test operation remains traced with workflow metadata absent. See HTTP and messaging tracing for optional Jaeger export and existing broker integration tests. No real lease workflow is installed by these tests.

On this page