diff --git a/src/content/docs/workflows/build/sleeping-and-retrying.mdx b/src/content/docs/workflows/build/sleeping-and-retrying.mdx
index aa84ed645d2ac4a..fcf44c79aa0665a 100644
--- a/src/content/docs/workflows/build/sleeping-and-retrying.mdx
+++ b/src/content/docs/workflows/build/sleeping-and-retrying.mdx
@@ -92,17 +92,6 @@ let someState = await step.do(
);
```
-## Access step context
-
-Workflow step callbacks receive a context object containing the current attempt number (1-indexed). This allows you to access which retry attempt is currently executing.
-
-```ts
-await step.do("my-step", async (ctx) => {
- // ctx.attempt is 1 on first try, 2 on first retry, etc.
- console.log(`Attempt ${ctx.attempt}`);
-});
-```
-
## Force a Workflow instance to fail
You can also force a Workflow instance to fail and _not_ retry by throwing a `NonRetryableError` from within the step.
diff --git a/src/content/docs/workflows/build/step-context.mdx b/src/content/docs/workflows/build/step-context.mdx
new file mode 100644
index 000000000000000..62c3e2cc18fc26c
--- /dev/null
+++ b/src/content/docs/workflows/build/step-context.mdx
@@ -0,0 +1,107 @@
+---
+title: Step context
+pcx_content_type: concept
+sidebar:
+ order: 5
+---
+
+Every `step.do` callback receives a **context object** (`WorkflowStepContext`) as its first argument. The context gives your step code runtime information about the step itself, the current retry attempt, and the resolved configuration for that step.
+
+## WorkflowStepContext
+
+```ts
+type WorkflowStepContext = {
+ step: {
+ name: string;
+ count: number;
+ };
+ attempt: number;
+ config: WorkflowStepConfig;
+};
+```
+
+### Properties
+
+| Property | Type | Description |
+| ------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| `step.name` | `string` | The name you passed to `step.do`. |
+| `step.count` | `number` | How many times `step.do` has been called with this name so far in the current Workflow run. Starts at `1` for the first call with a given name. |
+| `attempt` | `number` | The current attempt number (1-indexed). `1` on the first try, `2` on the first retry, and so on. |
+| `config` | [`WorkflowStepConfig`](/workflows/build/workers-api/#workflowstepconfig) | The resolved retry and timeout configuration for this step, including any defaults applied by the runtime. |
+
+## Access the context
+
+Pass a parameter to your `step.do` callback to receive the context object:
+
+```ts
+await step.do("my-step", async (ctx) => {
+ console.log(ctx.step.name); // "my-step"
+ console.log(ctx.step.count); // 1
+ console.log(ctx.attempt); // 1 on first try, 2 on first retry, etc.
+ console.log(ctx.config); // { retries: { limit: 5, ... }, timeout: "10 minutes" }
+});
+```
+
+The context is also available when you pass a custom `WorkflowStepConfig`:
+
+```ts
+await step.do(
+ "call an API",
+ {
+ retries: {
+ limit: 10,
+ delay: "10 seconds",
+ backoff: "exponential",
+ },
+ timeout: "30 minutes",
+ },
+ async (ctx) => {
+ console.log(ctx.config.retries.limit); // 10
+ console.log(ctx.config.timeout); // "30 minutes"
+ },
+);
+```
+
+## Examples
+
+### Adjust behavior based on retry attempt
+
+Use `ctx.attempt` to change how your step behaves on retries. For example, you might use a fallback endpoint after a certain number of retries:
+
+```ts
+await step.do(
+ "fetch data",
+ { retries: { limit: 5, delay: "5 seconds", backoff: "linear" } },
+ async (ctx) => {
+ const url =
+ ctx.attempt <= 3
+ ? "https://api.example.com/primary"
+ : "https://api.example.com/fallback";
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Request failed with status ${response.status}`);
+ }
+ return await response.json();
+ },
+);
+```
+
+### Log step metadata for observability
+
+Use `ctx.step` to add structured metadata to your logs:
+
+```ts
+await step.do("process-order", async (ctx) => {
+ console.log(
+ JSON.stringify({
+ step: ctx.step.name,
+ stepCount: ctx.step.count,
+ attempt: ctx.attempt,
+ retryLimit: ctx.config.retries?.limit,
+ }),
+ );
+
+ // Your step logic here
+});
+```
diff --git a/src/content/docs/workflows/build/workers-api.mdx b/src/content/docs/workflows/build/workers-api.mdx
index 47397e7d8ecb9f6..c731cb5ed8a94ce 100644
--- a/src/content/docs/workflows/build/workers-api.mdx
+++ b/src/content/docs/workflows/build/workers-api.mdx
@@ -76,12 +76,12 @@ Refer to the [events and parameters](/workflows/build/events-and-parameters/) do
### step
{/* prettier-ignore */}
-- step.do(name: string, callback: (): RpcSerializable): Promise<T>
-- step.do(name: string, config?: WorkflowStepConfig, callback: ():
+- step.do(name: string, callback: (ctx: WorkflowStepContext): RpcSerializable): Promise<T>
+- step.do(name: string, config?: WorkflowStepConfig, callback: (ctx: WorkflowStepContext):
RpcSerializable): Promise<T>
- `name` - the name of the step, up to 256 characters.
- `config` (optional) - an optional `WorkflowStepConfig` for configuring [step specific retry behaviour](/workflows/build/sleeping-and-retrying/).
- - `callback` - an asynchronous function that optionally returns serializable state for the Workflow to persist. In JavaScript Workflows, this includes a fresh, unlocked `ReadableStream` for large binary output.
+ - `callback` - an asynchronous function that receives a [`WorkflowStepContext`](/workflows/build/step-context/) and optionally returns serializable state for the Workflow to persist. In JavaScript Workflows, this includes a fresh, unlocked `ReadableStream` for large binary output.
:::note[Returning state]
@@ -193,6 +193,27 @@ export type WorkflowStepConfig = {
Refer to the [documentation on sleeping and retrying](/workflows/build/sleeping-and-retrying/) to learn more about how Workflows are retried.
+## WorkflowStepContext
+
+```ts
+export type WorkflowStepContext = {
+ step: {
+ name: string;
+ count: number;
+ };
+ attempt: number;
+ config: WorkflowStepConfig;
+};
+```
+
+- The `WorkflowStepContext` is passed as the first argument to the `step.do` callback function. It provides runtime information about the current step.
+ - `step.name` - the name of the step as passed to `step.do`.
+ - `step.count` - how many times `step.do` has been called with this name in the current Workflow run (1-indexed).
+ - `attempt` - the current attempt number (1-indexed). `1` on the first try, `2` on the first retry, and so on.
+ - `config` - the resolved `WorkflowStepConfig` for this step, including any defaults applied by the runtime.
+
+Refer to the [step context documentation](/workflows/build/step-context/) for usage examples.
+
## Workflow step limits
Each workflow on Workers Paid supports 10,000 steps by default. You can increase this up to 25,000 steps by configuring `steps` within the `limits` property of your Workflow definition in your Wrangler configuration:
diff --git a/src/content/docs/workflows/python/python-workers-api.mdx b/src/content/docs/workflows/python/python-workers-api.mdx
index aded391f4657b58..d9e0ca23a435662 100644
--- a/src/content/docs/workflows/python/python-workers-api.mdx
+++ b/src/content/docs/workflows/python/python-workers-api.mdx
@@ -155,6 +155,14 @@ class DemoWorkflowClass(WorkflowEntrypoint):
### Access step context (`ctx`)
+If you define a `ctx` parameter, the [step context](/workflows/build/step-context/) is injected into that argument. The context is a dictionary with the following keys:
+
+| Key | Type | Description |
+| --------- | ------ | ------------------------------------------------------------------------------------------------------ |
+| `step` | `dict` | Contains `name` (the step name) and `count` (how many times `step.do` has been called with this name). |
+| `attempt` | `int` | The current attempt number (1-indexed). |
+| `config` | `dict` | The resolved retry and timeout configuration for this step. |
+
```python
from workers import WorkflowEntrypoint
@@ -162,6 +170,10 @@ class CtxWorkflow(WorkflowEntrypoint):
async def run(self, event, step):
@step.do()
async def read_context(ctx):
+ print(ctx["step"]["name"]) # step name
+ print(ctx["step"]["count"]) # step count
+ print(ctx["attempt"]) # attempt number
+ print(ctx["config"]) # resolved step config
return ctx["attempt"]
return await read_context()