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.mdxSave 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:
| Content | What the author must explain |
|---|---|
| Purpose and scope | Business outcome, start and end, and exclusions. |
| Trigger and identity | What starts one instance and how retries recover the same identity. |
| Prerequisites | Required facts and their authoritative sources. |
| Steps | Stable IDs, dependencies, applicability, inputs, result, evidence, rules, timing, and exceptions. |
| Business rules | Conditions that must hold across steps, including calculations and duplicate prevention. |
| Timing | Deadlines and the events that start them, including calendar and timezone rules. |
| Exceptions and corrections | Allowed departures, approval, reasons, and correction of committed effects. |
| Completion and cancellation | Observable evidence for the overall outcome. |
| Examples | Concrete inputs and expected outcomes, including retries and conditional steps. |
| Open questions | Decisions that are missing; explicitly say none when resolved. |
| Implementation coverage | Automated, 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_chargesThe 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"
}
}| Field | Meaning |
|---|---|
definition_id | Stable specification ID, at most 128 characters. |
definition_version | Version selected for this instance, a positive integer string of at most nine digits. |
instance_id | UUID identifying one execution of the business workflow, independent of trace IDs. |
step_id | Specification step, at most 64 characters. |
action_id | UUID identifying one logical action, stable across retries. |
parent_action_id | Optional 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.MarshalHeadersFromContextwrites it with the event in the outbox transaction.- Publisher adapters pass
headers.Workflowastransport.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.NewClientsends the context in oneFlow-WorkflowJSON 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/loggingThey 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.