---
title: "StepOptions"
description: "A Step type returns StepOptions to customize its methods. WaitFor and"
canonical_url: "https://docs.superdurable.io/reference/go/step-options/"
source: "docs/reference/go/step-options.mdx"
locale: "en"
---

# StepOptions

A Step type returns **StepOptions** to customize its methods. **WaitFor** and
**Execute** have separate retry policies, timeouts, failure policies, state
selections, and lock sets.

<a id="retry-and-failure-policies"></a><a id="waitfor-failure-policy"></a><a id="execute-failure-policy"></a><a id="last-failure-info"></a><a id="retry-after"></a>Retry policies, failure policies, and **RetryAfter** are on
[Failure and retries](https://docs.superdurable.io/reference/go/failure-and-retries.md).

<a id="stepoptions-override-per-stepexecution"></a>

## Override StepOptions per Step execution

The **StepOptions** that a Step type returns apply to all of its Step
executions. To override them for one Step execution, pass **StepOptions** in
the Step movement that starts it.

```go
override := &dex.StepOptions{
	WaitForRetry:   &dex.RetryPolicy{MaximumAttempts: 2},
	WaitForFailure: dex.ProceedOnWaitForFailure,
}
return dex.GoTo(overrideSecond{}, payload, dex.WithStepOptions(override)), nil
```

Example: [examples/go/primitives/options-override/workflow.go](https://github.com/superdurable/dex/tree/main/examples/go/primitives/options-override/workflow.go)

<a id="durability"></a>

## Durability

With **SYNC** durability, Dex Server stores each attempt start before the
Worker receives the task. It also stores the Step completion before it goes on.

With **ASYNC** durability, Dex Server hands the task to the Worker, and starts
the next work, before the start and the completion are stored. It batches these
writes over about 20 milliseconds. Both modes reach about the same sustained
server throughput. **ASYNC** has lower latency, so a single Flow runs faster. On
object storage with low write latency, **ASYNC** also makes far fewer requests.

If a Dex Server node fails before an **ASYNC** result is stored, the result is
lost. The method runs again, and so does any later work that started from that
result. If the node fails before an **ASYNC** start is stored, Dex runs the same
attempt number again without using a retry. See
[An attempt starts when Dex hands it to a Worker](https://docs.superdurable.io/reference/go/failure-and-retries.md#same-attempt-redelivery).
Make **ASYNC** methods, and the work they start, idempotent.

Choose **SYNC** for effects that need the tightest guarantees. Choose **ASYNC**
for short Steps where latency matters and a rare rerun is acceptable. See
[Failure and redelivery](https://docs.superdurable.io/reference/go/failure-and-retries.md#failure-and-redelivery) for the full comparison.

**SYNC** is the default. To set the durability for a whole Flow, see
**StepDurability** in [FlowConfig](https://docs.superdurable.io/reference/go/flow.md#flow-config).

Retry policy, method timeout, and heartbeat settings apply to every attempt in
both modes.

```go
func (syncWork) GetStepOptions() *dex.StepOptions {
	return &dex.StepOptions{ExecuteDurability: dex.StepDurabilitySync}
}

func (asyncWork) GetStepOptions() *dex.StepOptions {
	return &dex.StepOptions{ExecuteDurability: dex.StepDurabilityAsync}
}
```

Example: [examples/go/primitives/durability/workflow.go](https://github.com/superdurable/dex/tree/main/examples/go/primitives/durability/workflow.go)

<a id="timeouts-and-total-duration"></a>

## Timeouts and total duration

Three options bound the time a Step method can take.

| Limit                   | Option                                               | Default                                                | Enforcement                                                                                                                                                                                                                |
| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Longest single attempt  | **WaitForMethodTimeout** or **ExecuteMethodTimeout** | Two hours. The server has no hard cap.                 | Dex times out the attempt. Another attempt starts only while **TotalDuration** has time left.                                                                                                                              |
| Time budget for retries | **TotalDuration** in the retry policy                | Four hours, for both durability modes.                 | After an attempt fails, Dex retries only while the budget has time left. It never stops an attempt in progress.                                                                                                            |
| Stalled Worker          | **HeartbeatTimeout**                                 | One minute. The default allowed minimum is 10 seconds. | Dex times out an attempt that sends no heartbeat or Stream message within this window. The retry policy then starts another attempt. See [Heartbeat](https://docs.superdurable.io/reference/go/step-options.md#heartbeat). |

The method timeout and the heartbeat timeout start when Dex sends the task to
the Worker. Waiting for locks or for a Worker, including time in the task
queue, does not count. The deadline in the task is the send time plus the
method timeout.

**TotalDuration** starts when Dex first hands attempt 1 to a Worker. If attempt
1 returns to pending without running, the clock restarts. Each retry waits in
the task queue again, and that wait counts toward **TotalDuration**. Dex
dispatches queued tasks roughly in the order they became ready, with no strict
order. Dex does not check **TotalDuration** while a retry waits in the queue,
so a retry that waited past it still runs. After a long backlog, a failed retry
may have no time left for another.

Dex checks **TotalDuration** only after an attempt fails, to decide whether to
retry. It never stops a running attempt, so the last attempt can start or end
after **TotalDuration**. Only the method timeout limits a single attempt: set
**ExecuteMethodTimeout** or **WaitForMethodTimeout**, not **TotalDuration**.

To bound a long wait, set the method timeout and **TotalDuration** to the same
maximum. Dex enforces both, so the Step needs no deadline of its own. While
Workers are available, the total time is at most about **TotalDuration** plus
one method timeout. Allow a few attempts with backoff in the retry policy, so
the Step recovers from transient errors and Worker crashes.

When a timeout expires, Dex fails the attempt with a timeout error. After
retries end, the failure policy decides what happens next. If no failure route
is configured, the Flow fails. See
[Execute Failure Recovery](https://docs.superdurable.io/design-patterns/failure-handling/execute-failure-recovery.md).
For a long wait on an external system, see
[Polling](https://docs.superdurable.io/design-patterns/polling.md).

<a id="heartbeat"></a>

## Heartbeat

During an attempt, the method must send a heartbeat or a
[Stream message](https://docs.superdurable.io/concepts/primitives/stream.md) within each **HeartbeatTimeout**.
A Stream message counts as a heartbeat.

For example, with an **Execute** method timeout of 60 seconds and a
**HeartbeatTimeout** of 10 seconds, Dex retries a stalled attempt after about
10 seconds instead of 60. To wait for an external system in
one heartbeating Step, use the [Polling](https://docs.superdurable.io/design-patterns/polling.md) pattern.

A heartbeat can carry a checkpoint value. The next attempt receives the latest
stored checkpoint and can resume from it. A heartbeat without a value clears
the checkpoint. A Stream message leaves the checkpoint as the last heartbeat
set it, including a cleared one. Dex can lose checkpoints; see
[Heartbeats are not acknowledged](https://docs.superdurable.io/reference/go/failure-and-retries.md#heartbeats-not-acknowledged).

```go
func (heartbeatStep) GetStepOptions() *dex.StepOptions {
	return &dex.StepOptions{
		ExecuteMethodTimeout: 60 * time.Second,
		HeartbeatTimeout:     10 * time.Second,
		ExecuteRetry:         &dex.RetryPolicy{MaximumAttempts: 3},
	}
}

func (step heartbeatStep) Execute(ctx dex.Context, batches int) (*dex.StepDecision, error) {
	completedBatches := 0
	if _, err := ctx.GetLastHeartbeatValue(&completedBatches); err != nil {
		return nil, err
	}
	for batch := completedBatches; batch < batches; batch++ {
		select {
		case <-ctx.Done():
			return dex.DeadEnd(), nil
		default:
		}
		time.Sleep(2 * time.Second)
		if err := ctx.RecordHeartbeat(batch + 1); err != nil {
			return nil, err
		}
	}
	return dex.GracefulComplete("processed"), nil
}
```

Example: [examples/go/primitives/heartbeat/workflow.go](https://github.com/superdurable/dex/tree/main/examples/go/primitives/heartbeat/workflow.go)

<a id="cancellation"></a>

## Cancellation

Dex sends a cancel request to the Worker when a Step is canceled, for example
by a **StepDecision** or an RPC, and when an attempt times out. The SDK also
cancels the method when the attempt deadline in the task passes. The method
receives the cancellation without having to return, fail, or send a heartbeat.

The SDK cannot stop running code. A long-running method must check for
cancellation and return, as the heartbeat example above does.
See [Make the Step stop](https://docs.superdurable.io/reference/server/runtime-data.md#cancel-honour) for the
check in each language, and
[Cancel running Steps](https://docs.superdurable.io/reference/server/runtime-data.md#cancel-running-steps) for
how Dex ends a canceled attempt.

Because the SDK cannot stop running code, a canceled attempt may still be
running when the next attempt or the failure route starts.

<a id="selective-state-loading"></a>

## Selective state loading

Every **WaitFor**, **Execute**, and timeout-handler call receives all ordinary
Attributes and size metadata for every Channel and ChannelMap instance.
AttributeMap values and pending Channel messages are not loaded unless that
method selects them.

**StepOptions** has separate selections for **WaitFor** and **Execute**. Select
a whole AttributeMap or ChannelMap when the method must go through its current
instances. Select one map instance when you already know its key. Locking an
Attribute does not load an AttributeMap value.

**WaitFor** and **Execute** each get their own snapshot. Dex takes the
**Execute** snapshot after the Wait consumed its messages, so **Execute** does
not see those messages again. Dex takes a method's snapshot when it hands
attempt 1 to a Worker. Retries of that method call, and attempts that return
to pending, reuse the same snapshot.

This Step loads the pending messages of the **Queued** Channel for **Execute**
only. **Execute** deletes the first pending message, and Dex applies the
deletion when the method succeeds.

```go
func (channelWait) GetStepOptions() *dex.StepOptions {
	return &dex.StepOptions{
		ExecuteLoadChannels: []dex.ChannelDef{Queued},
	}
}
```

Example: [examples/go/primitives/channel/workflow.go](https://github.com/superdurable/dex/tree/main/examples/go/primitives/channel/workflow.go)

Reading an AttributeMap value or pending Channel message that the method did
not select returns the SDK's not-loaded error. A selected empty Channel
returns an empty message list. Sizes and map keys are available without
loading. For Go Flows, **dexcli visualize** reports a Step method that reads
state it did not select. See
[State loads](https://docs.superdurable.io/reference/web/flow-visualization.md#state-loads).

<a id="attribute-locks"></a>

## Attribute locks

Steps and RPCs that run at the same time can read and write the same
Attribute. To prevent races, have each of them lock that Attribute. Dex Server
then lets only one Step method or RPC hold the lock at a time. Set the locks in
**StepOptions**. **WaitFor** and **Execute** have separate lock sets.

See [Attribute locks](https://docs.superdurable.io/reference/server/runtime-data.md#attribute-locks) for when
a Step attempt takes and holds its locks, with **WaitFor** and **Execute**
examples. See [Locks](https://docs.superdurable.io/concepts/primitives/attribute.md#locking) for the lock model.
