Running and observing
Starting, status, listing, cancelling, retention, and purging
The operations are bound to an actor.Service via Service(...), which you obtain from a host with host.Service():
svc := orders.Service(host.Service())Starting#
// Without WithInstanceID, the engine generates a UUIDv7
id, created, err := svc.Start(ctx, OrderInput{OrderID: "A-91", Total: 4999})
// Use a natural key to make starting idempotent
id, created, err = svc.Start(ctx, input, workflow.WithInstanceID("order-A-91"))Start returns after the workflow is scheduled, and does not wait for the workflow to complete.
A second Start with an instance ID that already exists finds the first: created comes back false and the second call’s input is discarded.
An instance ID that has already terminated is not restarted, so re-driving a failed run needs a fresh ID.
The input is JSON-encoded and capped by WithMaxInputSize (64 KiB by default). An oversized input returns ErrInputTooLarge.
Status#
status, err := svc.GetStatus(ctx, id)
if errors.Is(err, workflow.ErrInstanceNotFound) {
// No such instance, or it was purged after passing its retention
}For child steps, StepStatusView.Children pairs each child instance ID with the status and compensation outcome it reported. The outcome stays on the parent after the child is purged.
GetStatus is a read, so polling it is cheap.
type InstanceStatus struct {
InstanceID string
Workflow string
Version int
Status Status
Compensation CompensationOutcome
CurrentStep string
Steps []StepStatusView // every step: status, task counts, attempts, timings, error, and child outcomes
Cause string
Output json.RawMessage // what the run produced, once it has completed
Suspended *SuspendView // when suspended: since when, why, and what it was
Parent *ParentView // when a child: whose
CreatedAt, StartedAt, CompletedAt time.Time
}The output is the output of the step named with WithOutput, or of the last step otherwise. It is set once the instance completes and readable for as long as the instance is retained. Decode it with DecodeOutput:
var result CheckoutResult
err = status.DecodeOutput(&result)It is empty for any instance that has not completed, and DecodeOutput then leaves the destination untouched.
A caller never sees completed before every step has finished, including the optional ones.
The possible statuses are:
| Status | Meaning |
|---|---|
pending | accepted and durable, but not started yet |
running | executing its steps |
suspended | paused by Suspend |
compensating | unwinding its compensation stack |
completed | ran to the end successfully (terminal) |
failed | could not complete (terminal) |
cancelled | cancelled and unwound (terminal) |
Listing#
page, err := svc.List(ctx, &workflow.ListOptions{
Status: workflow.StatusRunning, // or any status - empty means all
Version: 3, // optional
Parent: parentID, // optional: the children of one instance
Limit: 50,
After: page.AfterID(), // pagination cursor, an instance ID
})Each of status, version, and parent is indexed. There is nothing to configure, and those three are the only filters.
Because the default instance ID is a UUIDv7, a listing is in creation order. Page until AfterID() returns empty:
var cursor string
for {
page, err := svc.List(ctx, &workflow.ListOptions{
Status: workflow.StatusFailed,
After: cursor,
Limit: 100,
})
if err != nil {
return err
}
for _, inst := range page.Instances {
// ...
}
cursor = page.AfterID()
if cursor == "" {
break
}
}Cancelling#
err := svc.Cancel(ctx, id, "customer cancelled the order")Cancel moves a running or suspended instance into compensating and rolls back its stack, with the reason recorded as the cause every compensation receives. Work that has not started is dropped, and an attempt already running is not interrupted. The instance terminates as cancelled.
Retention and purging#
WithRetention takes one duration per terminal status:
workflow.WithRetention(workflow.RetentionPolicy{
Completed: 24 * time.Hour,
Failed: 7 * 24 * time.Hour,
Cancelled: 7 * 24 * time.Hour,
})A purge removes a terminated instance: its children, the jobs it used, and its recorded history. Every workflow automatically sweeps instances past their retention every 12 hours. An instance that is never swept expires on its own as a backstop.
You can purge at three levels:
// One terminated instance, and everything it owns
err := svc.Purge(ctx, id)
// Every terminated instance past its retention, skipping any that still has a parent
n, err := svc.PurgeTerminated(ctx)// Override the default with a fixed interval
workflow.WithAutoPurgeInterval(6 * time.Hour)
// Or use a cron schedule instead
workflow.WithAutoPurgeCron("0 3 * * *")Purge refuses a running or suspended instance with ErrInstanceActive. It is idempotent, so an interrupted purge is safe to repeat. It also refuses an instance whose parent is still running.
PurgeTerminated works through a backlog in pages. However many hosts registered the workflow, automatic purging runs the sweep on one of them per schedule.
Once an instance is purged, GetStatus reports not found. An instance’s history is an operational record, not an audit log. If you need a permanent one, write it from a step, as the thumbnails example
does.