# Steps

> Learn how to communicate with external APIs and services

When using DBOS workflows, you should call any function that performs complex operations or accesses external APIs or services as a _step_.
If a workflow is interrupted, upon restart it automatically resumes execution from the **last completed step**.

You can use `DBOS.runStep` to call a function as a step.  For a function to be used as a step, it should have a return value that can be serialized as JSON, and should not have non-durable side effects.

Here's a simple example:

```javascript
async function generateRandomNumber() {
  return Math.random();
}

async function workflowFunction() {
  const randomNumber = await DBOS.runStep(() => generateRandomNumber(), {name: "generateRandomNumber"});
}
const workflow = DBOS.registerWorkflow(workflowFunction)
```

Alternatively, you can register a function as a step using `DBOS.registerStep`:

```javascript
async function generateRandomNumber() {
  return Math.random();
}
const randomStep = DBOS.registerStep(generateRandomNumber);

async function workflowFunction() {
  const randomNumber = await randomStep();
}
const workflow = DBOS.registerWorkflow(workflowFunction)
```

Or use the `@DBOS.step()` decorator:

```typescript
export class Example {
  @DBOS.step()
  static async generateRandomNumber() {
    return Math.random();
  }

  @DBOS.workflow()
  static async exampleWorkflow() {
    await Example.generateRandomNumber();
  }
}
```

You should make a function a step if you're using it in a DBOS workflow and it performs a [**nondeterministic**](../tutorials/workflow-tutorial.md#determinism) operation.
A nondeterministic operation is one that may return different outputs given the same inputs.
Common nondeterministic operations include:

- Accessing an external API or service, like serving a file from [AWS S3](https://aws.amazon.com/s3/), calling an external API like [Stripe](https://stripe.com/), or accessing an external data store like [Elasticsearch](https://www.elastic.co/elasticsearch/).
- Accessing files on disk.
- Generating a random number.
- Getting the current time.

You **cannot** call, start, or enqueue workflows from within steps.
These operations should be performed from workflow functions.
You can call one step from another step, but the called step becomes part of the calling step's execution rather than functioning as a separate step.
If you call a step from outside a workflow (after DBOS is launched), it runs as an ordinary function, without checkpoints, retries, or timeouts.

### Configurable Retries

You can optionally configure a step to automatically retry any exception a set number of times with exponential backoff.
This is useful for automatically handling transient failures, like making requests to unreliable APIs.
Retries are configurable through the `StepConfig`, which can be passed to `runStep`, `registerStep`, or the step decorator.

```typescript
export interface StepConfig {
  retriesAllowed?: boolean; // Should failures be retried? (default false)
  intervalSeconds?: number; // Seconds to wait before the first retry attempt (default 1).
  maxAttempts?: number;     // Maximum number of attempts, including the first (default 3). If every attempt fails, throw an exception.
  backoffRate?: number;     // Multiplier by which the retry interval increases after a retry attempt (default 2).
  shouldRetry?: (error: unknown) => boolean | Promise<boolean>; // Predicate called after a failure to decide whether to retry (default: retry every error).
  timeoutMS?: number;       // Maximum duration of a single step attempt, in milliseconds. An attempt exceeding it fails with DBOSStepTimeoutError.
  name?: string;            // Name of the step (defaults to the function name).
}
```

For example, let's configure this step to retry exceptions (such as if `example.com` is temporarily down), making up to 10 attempts:

```javascript
async function fetchFunction() {
    return await fetch("https://example.com").then(r => r.text());
}

async function workflowFunction() {
    const randomNumber = await DBOS.runStep(() => fetchFunction(), {
        name: "fetchFunction",
        retriesAllowed: true,
        maxAttempts: 10
    });
}
```

Or if registering the step:

```javascript
async function fetchFunction() {
    return await fetch("https://example.com").then(r => r.text());
}
const fetchStep = DBOS.registerStep(fetchFunction, {
    retriesAllowed: true,
    maxAttempts: 10
});
```

Or if using decorators:

```javascript
@DBOS.step({retriesAllowed: true, maxAttempts: 10})
static async exampleStep() {
  return await fetch("https://example.com").then(r => r.text());
}
```

If a step fails on all `maxAttempts` attempts, it throws an exception (`DBOSMaxStepRetriesError`) to the calling workflow.
If that exception is not caught, the workflow terminates.

#### Filtering Retries With `shouldRetry`

By default, every error thrown by the step is retried until `maxAttempts` is reached.
If you only want to retry certain errors; for example, transient network errors but not validation failures, pass a `shouldRetry` predicate.
The predicate receives the thrown error. If it returns `false` (or a promise resolving to `false`), the error is re-thrown immediately and no further retries are attempted.

```typescript
await DBOS.runStep(() => fetchFunction(), {
  name: "fetchFunction",
  retriesAllowed: true,
  maxAttempts: 10,
  shouldRetry: (e) => !(e instanceof FatalError),
});
```

The predicate may be async, and it works the same way on `runStep`, `registerStep`, and the `@DBOS.step` decorator.
If the predicate itself throws or rejects, that error is recorded as the step's failure and propagated to the workflow.
`shouldRetry` is ignored when `retriesAllowed` is `false`.

### Step Timeouts

You can set a timeout on a step by passing `timeoutMS` to its [`StepConfig`](../reference/workflows-steps.md#dbosstep).
If a single attempt of the step runs longer than the timeout, it fails with a `DBOSStepTimeoutError`.
If `retriesAllowed` is `true`, a timed-out attempt is retried like any other failure.

```typescript
await DBOS.runStep(() => callSlowService(), {
  name: "callSlowService",
  timeoutMS: 5000, // Fail this attempt if it runs longer than 5 seconds
});
```

Step timeouts are **cooperative**: DBOS does not forcibly terminate a running step.
When the timeout expires, DBOS aborts the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) exposed at [`DBOS.stepStatus.timeoutSignal`](../reference/methods.md#dbosstepstatus) and the step's attempt fails.
A step that does not observe this signal keeps running in the background, but its result is discarded.
To stop work promptly when a step times out, pass the signal to APIs that accept one (for example, `fetch`):

```typescript
async function fetchData() {
  // fetch aborts as soon as the step's timeout fires
  const response = await fetch("https://example.com", { signal: DBOS.stepStatus?.timeoutSignal });
  return await response.text();
}

async function workflowFunction() {
  return await DBOS.runStep(() => fetchData(), { name: "fetchData", timeoutMS: 5000 });
}
```

A fresh `timeoutSignal` is issued for each retry attempt.

Independently of any timeout, [`DBOS.stepStatus.cancelSignal`](../reference/methods.md#dbosstepstatus) fires if the step's workflow is [cancelled](./workflow-management.md#cancelling-workflows).
To stop a step promptly on either a timeout or a cancellation, combine the two signals:

```typescript
async function fetchData() {
  // timeoutSignal is set because this step is run with timeoutMS
  const { timeoutSignal, cancelSignal } = DBOS.stepStatus!;
  const signal = AbortSignal.any([timeoutSignal!, cancelSignal]);
  const response = await fetch("https://example.com", { signal });
  return await response.text();
}

async function workflowFunction() {
  return await DBOS.runStep(() => fetchData(), { name: "fetchData", timeoutMS: 5000 });
}
```
