Documentation
¶
Overview ¶
Package temporalworkflow implements workflow.Driver on Temporal workflows and activities.
Two types split the work. NewRunner runs in the worker process: it registers the workflows and steps of a workflow.Registry on a [temporal.Host]. NewDriver runs in any process that starts and controls runs: it uses only the client of the host, so the host can be client-only with [temporal.Builder.NoWorker].
Construction uses DriverBuilder or RunnerBuilder, separately from workflow declarations and host construction. Policies run once in order on one fresh draft; nil entries panic and expanded nil or empty slices omit edits. Only DriverBuilder exposes Registry; NewRunner's required registry supplies its driver registry. Published settings detach from retained drafts, not services.
driver, err := temporalworkflow.NewDriver(temporal.New(), func(driver *temporalworkflow.DriverBuilder) {
driver.Prefix("orders")
})
if err != nil {
panic(err)
}
_ = workflow.NewClient(driver)
Names ¶
The workflow "onboard" with the default prefix is the Temporal workflow type "arkoren-wf-onboard", the step "send-welcome" is the activity type "arkoren-step-send-welcome", and the signal "email-confirmed" is the signal "arkoren.sig.email-confirmed". The workflow ID is the run id of the facade. Payloads are the JSON of the facade as raw JSON values, so the Temporal UI shows them as they are.
Semantics ¶
- Start: conflict policy FAIL and reuse policy REJECT_DUPLICATE, so a second Start with the same id is workflow.ErrAlreadyStarted, also after the run ended. Delay is the Temporal start delay and Metadata is the memo. With DriverBuilder.Registry (the driver of a Runner has it) the execution timeout is MaxDuration plus Delay and an unregistered name is workflow.ErrNotFound.
- MaxDuration: the runner starts a timer when the workflow starts; when it fires, the context of the run is canceled, blocking calls return an error that matches workflow.ErrTimeout, and the run fails with that error. The server execution timeout (MaxDuration plus Delay, set when the driver knows the registry) is only a backstop for a stuck run; it ends the run as timed out, which is also workflow.ErrTimeout and workflow.Failed.
- Cancel: CancelWorkflow. The context of the run is canceled, so Sleep, Wait, Select, Step, and Child return workflow.ErrCanceled and the run ends as Canceled; Result is workflow.ErrCanceled and Status is workflow.Canceled. The driver describes the named execution before observations and commands; a wrong workflow name is workflow.ErrNotFound and never issues a cancel or signal. Finished known runs cancel without error. Native clients need description permission in addition to command permissions. These are typed-handle checks, not application authorization.
- Signal: SignalWorkflow, which buffers the value until the workflow reads it. An unknown or finished run is workflow.ErrNotFound. The facade keeps the first value per signal; later values stay unread.
- Status: Running, and ContinuedAsNew, are workflow.Running; Completed is workflow.Completed; Failed, TimedOut, and Terminated are workflow.Failed; Canceled is workflow.Canceled. Result of a terminated run is an error of kind "temporal.terminated".
- Failures: a failing workflow or step ends with an application error of type "arkoren.error" whose detail is the workflow.EncodeError envelope, so error kinds survive. A workflow.Permanent failure is a non-retryable application error.
- Steps run as activities with a start-to-close timeout of Timeout and a Temporal retry policy built from Attempts, Initial, Factor, and Max. Per-call options override them. Timeout zero removes the attempt limit; the workflow MaxDuration supplies the required native schedule-to-close bound instead. A timeout is workflow.ErrTimeout. A positive Max caps even the first retry interval; zero Max requests Go's largest duration rather than Temporal's implicit 100-times-initial cap. Backend duration limits can reduce that request: full Go-duration portability is not promised. Multiple attempts with Initial zero return workflow.ErrUnsupported before activity encoding or scheduling; no approximate positive delay is inserted. A single attempt remains supported because backoff is unused. The completed local Step.Derive policy is authoritative, not the registered default.
- Child runs a child workflow with the execution timeout of its MaxDuration and waits. Parent cancellation requests cancellation of an owned native child, but ends the runtime wait without requiring child callbacks to stop. The child id is joined when a run with it exists: a workflow cannot join a run, so the runner waits for it in an activity ("<prefix>-join") that reads the result through the client; the run must use the same task queue and the activity is retried at most three times. Spawn starts the child with the parent close policy ABANDON on a context that is disconnected from the parent, and waits only until the child started. Canceling observation of an already existing run does not cancel that run. Spawn's acknowledgement wait still ends on parent cancellation or MaxDuration; the detached child might have started even when that wait returns an error. Creation versus joining is decided by Temporal's atomic child-start acknowledgement, not a status lookup. Only newly created children have parent-close policy TERMINATE; an existing run retains its original owner, including after parent MaxDuration. This clarification adds no history command or marker and does not change the existing child/join sequence.
- Wait and Select use a Temporal selector over the signal channels, the cancellation of the run, and timers. Timers that lose are canceled and signals that no case consumed stay buffered.
- Now is the workflow time, UUID is derived from the run ID of the execution and a counter (no history event), and Logger writes to the Temporal workflow logger, which is silent while the workflow replays. LogValuers, including nested groups and bound attributes, are resolved before crossing into native logging so slog redaction applies. Raw returns the go.temporal.io/sdk/workflow.Context of the run.
- Done and Err: Err reads the Temporal context, so it is deterministic and accurate as soon as the cancellation was delivered. Done is a native channel that is closed when a runtime call or Err sees the context done, or when a coroutine of the runtime that waits on the Temporal channel is scheduled. Poll it with a non-blocking select; do not block on it with "<-ctx.Done()": Temporal cannot schedule a native block and fails the workflow task as a deadlock. Use Sleep, Wait, or Select to block.
- Retention is a setting of the Temporal namespace and is ignored, like Description.
Retry-policy compatibility and rollout ¶
Core step defaults remain supported: one attempt, or a positive one-second initial delay when Retry enables more attempts. Whole-workflow execution retry policy remains absent; finite application step retries are native activity retries. Omitting an activity retry policy would enable native server defaults, including potentially unlimited attempts, so this driver never uses omission to disable application retries.
The previous driver silently replaced zero Initial with a positive native interval. Rejecting that policy changes command selection and is not safe as an unqualified in-place worker upgrade for existing zero-policy histories. Keep those executions on legacy workers using application-owned worker routing until they finish, or drain them before upgrading. Explicitly choosing the old positive interval can preserve equivalent histories; replay your actual histories before deployment. The driver adds no version marker and does not use IsReplaying to suppress policy rejection.
Max zero avoids the native implicit 100-times-initial cap, not every backend duration bound. Native server versions can cap requested durations. Inspect actual server version and effective policy when engine limits matter; do not infer the server version from a CLI image tag or assume a universal maximum. The integration-tested two-attempt policy on server 1.29.1 retained 876000 hours (100 times 365 days), rather than the requested largest Go duration. This is evidence for that server and policy, not a duration guarantee for every backend.
Telemetry ¶
Configure workflow.RegistryBuilder.Telemetry in the worker registry, separate from workflow.ClientBuilder.Telemetry in each client process. StartOptions.Trace travels in a dedicated argument envelope, never memo. Replay forwards it as deterministic data through activities and child/spawn runs; real activities extract it before invoking the registered step. Signals keep the start parent. No host propagator hook is needed. Deploy upgraded workers before enabling traced producers; old workers cannot decode traced envelopes. Legacy untraced histories retain their argument shape. Export every pod to a common backend; client Result observations measure waiting, not durable run duration.
Caveats ¶
- NewRunner fails on a host built with [temporal.Builder.NoWorker]. A process that sets Builder.NoWorker is client-only and uses NewDriver; a runner on such a host would never run a workflow.
- Temporal ends a blocked workflow coroutine with runtime.Goexit, which recover does not catch, so the panic recovery of the facade does not swallow it. The runner needs no special handling. A panic in workflow code becomes a workflow failure, like in the other drivers, and not a retried workflow task.
- The workflow function replays, so it must be deterministic. Do not start goroutines that call the context.
- The runner and the drivers that talk to it must use the same prefix and the same codec in workflow.HandlersWith and workflow.ClientBuilder.Codec.
- Workflow IDs are unique per namespace, so the same run id of two different workflows collides.
- A step that fails makes the Temporal worker log an error line for every attempt.
Index ¶
- Constants
- type Driver
- func (driver *Driver) Cancel(ctx context.Context, name, id string) error
- func (driver *Driver) Result(ctx context.Context, name, id string) ([]byte, error)
- func (driver *Driver) Signal(ctx context.Context, name, id, signal string, v []byte) error
- func (driver *Driver) Start(ctx context.Context, name, id string, in []byte, options workflow.StartOptions) error
- func (driver *Driver) Status(ctx context.Context, name, id string) (workflow.Status, error)
- type DriverBuilder
- type DriverBuilderFunc
- type Runner
- type RunnerBuilder
- type RunnerBuilderFunc
Constants ¶
const DefaultPrefix = "arkoren"
DefaultPrefix is the name prefix used when DriverBuilder.Prefix is empty.
Example:
component, err := NewDriver(temporal.New(), func(driver *DriverBuilder) {
driver.Prefix(DefaultPrefix)
})
if err != nil {
panic(err)
}
_ = component
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Driver ¶
type Driver struct {
// contains filtered or unexported fields
}
Driver is a workflow.Driver that starts and controls runs through the Temporal client of a host. It runs no workflow code; the Runner of the worker process does. A Driver is safe for concurrent use. Typed observations and commands verify the persisted native workflow type; namespace-wide ID uniqueness does not permit a wrong-name handle to use it.
Example:
driver, err := NewDriver(temporalhost.New())
if err != nil {
return err
}
_ = workflow.NewClient(driver)
func NewDriver ¶
func NewDriver(host *temporalhost.Host, configure ...DriverBuilderFunc) (*Driver, error)
NewDriver returns a Driver that talks to Temporal through host. The host needs no worker for that; use temporalhost.Builder.NoWorker in a process that only starts runs. Call it before or after host.Start; calls fail until the host has a native client. A NewFrom client is available immediately, whereas a dialing host acquires its client during Start. It performs no I/O. Callbacks run once in order on one fresh DriverBuilder; nil entries panic. Published settings are independent of a retained draft. DriverBuilder.Registry optionally supplies timeout and registration knowledge to a client-only driver.
Example:
driver, err := NewDriver(temporalhost.New(), func(driver *DriverBuilder) {
driver.Prefix("orders")
})
if err != nil {
panic(err)
}
_ = driver
func (*Driver) Cancel ¶
Cancel verifies the named native run before requesting cancellation. A finished known run is not an error, including when it finishes between the description and cancellation request. Unknown IDs or mismatched workflow names return workflow.ErrNotFound without issuing a cancellation request.
Example:
if err := driver.Cancel(ctx, "onboard", "user-42"); err != nil {
return err
}
func (*Driver) Result ¶
Result waits for the run to end. A canceled run gives workflow.ErrCanceled, a run that hit its execution timeout gives workflow.ErrTimeout, and other failures are decoded from the error envelope the Runner stored. A wrong workflow name returns ErrNotFound before reading output; a native timeout outranks its last application-failure cause.
Example:
output, err := driver.Result(ctx, "onboard", "user-42")
if err != nil {
return err
}
_ = output // Encoded facade output, not a native Temporal payload wrapper.
func (*Driver) Signal ¶
Signal sends the signal to the run. It returns workflow.ErrNotFound for an unknown or finished run. Temporal buffers a signal until the workflow reads it. A mismatched workflow name is rejected before delivery; description and command permissions are both required on the native client.
Example:
if err := driver.Signal(ctx, "onboard", "user-42", "approved", []byte(`{"v":true}`)); err != nil {
return err
}
func (*Driver) Start ¶
func (driver *Driver) Start(ctx context.Context, name, id string, in []byte, options workflow.StartOptions) error
Start starts run id of the workflow called name with a workflow ID of id. It returns workflow.ErrAlreadyStarted when Temporal knows a run with this ID, also a finished one, and workflow.ErrNotFound when the configured registry lacks the workflow. StartOptions.Delay is the Temporal start delay and StartOptions.Metadata becomes the memo.
Example:
err := driver.Start(ctx, "onboard", "user-42", []byte(`{"v":"Ada"}`), workflow.StartOptions{})
if err != nil && !errors.Is(err, workflow.ErrAlreadyStarted) {
return err
}
func (*Driver) Status ¶
Status describes the run. Completed, failed, timed out, and terminated runs map to workflow.Completed or workflow.Failed, a canceled run to workflow.Canceled, and a run that continued as new to workflow.Running. Missing IDs and mismatched workflow names return ErrNotFound.
Example:
status, err := driver.Status(ctx, "onboard", "user-42")
if err != nil {
return err
}
_ = status == workflow.Running
type DriverBuilder ¶
type DriverBuilder struct {
// contains filtered or unexported fields
}
DriverBuilder edits the construction settings of NewDriver. Callbacks share one fresh draft and run once in order; a nil callback panics. Retaining a draft does not permit changes to the published component. Dependencies and native SDK internals retain their existing ownership.
Example:
component, err := NewDriver(temporal.New(), func(driver *DriverBuilder) {
driver.Logger(slog.Default())
})
if err != nil {
panic(err)
}
_ = component
func (*DriverBuilder) Logger ¶
func (driver *DriverBuilder) Logger(logger *slog.Logger) *DriverBuilder
Logger receives log lines of the driver. Nil uses slog.Default. It returns the same draft for subsequent settings.
Example:
component, err := NewDriver(temporal.New(), func(driver *DriverBuilder) {
driver.Logger(slog.Default())
})
if err != nil {
panic(err)
}
_ = component
func (*DriverBuilder) Prefix ¶
func (driver *DriverBuilder) Prefix(prefix string) *DriverBuilder
Prefix names the Temporal workflow types ("<prefix>-wf-<name>"), the activity types ("<prefix>-step-<name>"), and the signals ("<prefix>.sig.<name>"). Empty uses DefaultPrefix. A runner and the drivers that talk to it must use the same prefix. It returns the same draft for subsequent settings.
Example:
component, err := NewDriver(temporal.New(), func(driver *DriverBuilder) {
driver.Prefix("example")
})
if err != nil {
panic(err)
}
_ = component
func (*DriverBuilder) Registry ¶
func (driver *DriverBuilder) Registry(registry *workflow.Registry) *DriverBuilder
Registry lets a Driver know the workflows: it sets the Temporal execution timeout from MaxDuration and returns workflow.ErrNotFound for an unregistered name. A Runner fills it in for its own driver. Without it the run has no server-side execution timeout; the runner still enforces MaxDuration inside the workflow. It returns the same draft for subsequent settings.
Example:
component, err := NewDriver(temporal.New(), func(driver *DriverBuilder) {
driver.Registry(nil)
})
if err != nil {
panic(err)
}
_ = component
type DriverBuilderFunc ¶
type DriverBuilderFunc = func(*DriverBuilder)
DriverBuilderFunc configures a fresh client-driver draft during NewDriver. Callbacks run once synchronously in order; nil entries panic. Native host and registry dependencies remain shared after settings publication.
Example:
policies := []DriverBuilderFunc{
func(driver *DriverBuilder) {
driver.Prefix("orders")
},
}
driver, err := NewDriver(temporal.New(), policies...)
if err != nil {
return err
}
_ = workflow.NewClient(driver)
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner serves the workflows and steps of a workflow.Registry on the worker of a temporal.Host (package arkoren.dev/driver/temporal). Create it with NewRunner before the host starts. It shares the registry's handlers and native host; it does not own that host's lifecycle. Workflow functions replay and must remain deterministic.
Example:
registry, err := workflow.Handlers()
if err != nil {
return err
}
runner, err := NewRunner(temporalhost.New(), registry)
if err != nil {
return err
}
_ = workflow.NewClient(runner.Driver())
func NewRunner ¶
func NewRunner(host *temporalhost.Host, registry *workflow.Registry, configure ...RunnerBuilderFunc) (*Runner, error)
NewRunner registers every workflow of registry as the Temporal workflow "<prefix>-wf-<name>" and every step as the activity "<prefix>-step-<name>" on host. It returns temporalhost.ErrStarted when host is already started. The workflows run on the task queue of the host.
Cancellation, MaxDuration, and step retries follow the rules in the package documentation. Callbacks run once in order on one fresh RunnerBuilder; nil entries panic. The required registry always supplies the runner's driver registry; the runner draft has no Registry setting. Nil hosts and registries remain errors.
Example:
registry, err := workflow.Handlers()
if err != nil {
panic(err)
}
runner, err := NewRunner(temporalhost.New(), registry, func(runner *RunnerBuilder) {
runner.Prefix("orders")
})
if err != nil {
panic(err)
}
_ = runner.Driver()
func (*Runner) Driver ¶
Driver returns the workflow.Driver of the runner. It starts runs on the same host and sees the workflows of the registry. The returned pointer is shared and safe for concurrent command use; this transfers no host ownership.
Example:
runner, err := NewRunner(host, registry)
if err != nil {
return err
}
client := workflow.NewClient(runner.Driver())
_ = client
type RunnerBuilder ¶
type RunnerBuilder struct {
// contains filtered or unexported fields
}
RunnerBuilder edits the construction settings of NewRunner. Callbacks share one fresh draft and run once in order; a nil callback panics. Retaining a draft does not permit changes to the published component. Dependencies and native SDK internals retain their existing ownership.
Example:
registry, err := workflow.Handlers()
if err != nil {
panic(err)
}
component, err := NewRunner(temporal.New(), registry, func(runner *RunnerBuilder) {
runner.Logger(slog.Default())
})
if err != nil {
panic(err)
}
_ = component
func (*RunnerBuilder) Logger ¶
func (runner *RunnerBuilder) Logger(logger *slog.Logger) *RunnerBuilder
Logger receives log lines of the driver. Nil uses slog.Default. It returns the same draft for subsequent settings.
Example:
registry, err := workflow.Handlers()
if err != nil {
panic(err)
}
component, err := NewRunner(temporal.New(), registry, func(runner *RunnerBuilder) {
runner.Logger(slog.Default())
})
if err != nil {
panic(err)
}
_ = component
func (*RunnerBuilder) Prefix ¶
func (runner *RunnerBuilder) Prefix(prefix string) *RunnerBuilder
Prefix names the Temporal workflow types ("<prefix>-wf-<name>"), the activity types ("<prefix>-step-<name>"), and the signals ("<prefix>.sig.<name>"). Empty uses DefaultPrefix. A runner and the drivers that talk to it must use the same prefix. It returns the same draft for subsequent settings.
Example:
registry, err := workflow.Handlers()
if err != nil {
panic(err)
}
component, err := NewRunner(temporal.New(), registry, func(runner *RunnerBuilder) {
runner.Prefix("example")
})
if err != nil {
panic(err)
}
_ = component
type RunnerBuilderFunc ¶
type RunnerBuilderFunc = func(*RunnerBuilder)
RunnerBuilderFunc configures a fresh worker-runner draft during NewRunner. Callbacks run once synchronously in order; nil entries panic. The positional registry is authoritative for both runner execution and its client driver.
Example:
policies := []RunnerBuilderFunc{
func(runner *RunnerBuilder) {
runner.Prefix("orders")
},
}
registry, err := workflow.Handlers()
if err != nil {
return err
}
runner, err := NewRunner(temporal.New(), registry, policies...)
if err != nil {
return err
}
_ = runner.Driver()
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
execution
Package execution owns deterministic Temporal workflow execution.
|
Package execution owns deterministic Temporal workflow execution. |
|
protocol
Package protocol owns Temporal raw JSON and native failure envelopes.
|
Package protocol owns Temporal raw JSON and native failure envelopes. |