# Versioning

> Change workflows while preserving progress for runs already in flight.

You can deploy new function code while runs are waiting or sleeping. When a run resumes, Inngest uses the current code and reuses results from completed steps whose IDs still match.

## How Inngest handles versioning

Each [step](/docs-markdown/durable-execution/primitives) has an ID. Inngest saves the result when the step completes. On resume, the SDK returns that result for the same step ID instead of running the step again. If it finds a new ID, it runs that step and saves its result. You do not need version markers for compatible changes.

For steps inside loops, the SDK combines the step ID with an occurrence counter. This gives each pass through the loop its own saved result. See [durable execution concepts](/docs-markdown/durable-execution/concepts) for the execution model.

## Evolving functions over time

New runs use your latest code. For a run already in progress, the effect depends on what you change:

| Change                               | Effect on an in-progress run                                                          |
| ------------------------------------ | ------------------------------------------------------------------------------------- |
| Add a step                           | Inngest runs it when the new code reaches it.                                         |
| Change a step's code and keep its ID | A completed step returns its saved result. A step that has not run uses the new code. |
| Change a step's ID                   | Inngest treats it as a new step and runs it when reached.                             |
| Remove a step                        | The new code no longer calls it. Its saved result remains unused.                     |
| Reorder steps                        | Completed steps return their saved results by ID. The SDK may log an order warning.   |

For example, add `track-signup` between two existing steps:

```ts {{ title: "TypeScript" }}
await step.run("send-welcome-email", () => sendWelcomeEmail(event.data.email));
await step.run("track-signup", () => analytics.track("user_signup_complete", event.data));
await step.run("sync-to-crm", () => crm.contacts.create(event.data));
```

```python {{ title: "Python" }}
async def send_email() -> None:
    await send_welcome_email(ctx.event.data["email"])

async def track_signup() -> None:
    await analytics.track("user_signup_complete", ctx.event.data)

async def sync_to_crm() -> None:
    await crm.contacts.create(ctx.event.data)

await ctx.step.run("send-welcome-email", send_email)
await ctx.step.run("track-signup", track_signup)
await ctx.step.run("sync-to-crm", sync_to_crm)
```

```go {{ title: "Go" }}
_, err := step.Run(ctx, "send-welcome-email", func(ctx context.Context) (any, error) {
	return nil, sendWelcomeEmail(ctx, input.Event.Data.Email)
})
if err != nil {
	return nil, err
}
_, err = step.Run(ctx, "track-signup", func(ctx context.Context) (any, error) {
	return nil, analytics.Track(ctx, "user_signup_complete", input.Event.Data)
})
if err != nil {
	return nil, err
}
_, err = step.Run(ctx, "sync-to-crm", func(ctx context.Context) (any, error) {
	return nil, crm.Contacts.Create(ctx, input.Event.Data)
})
```

If a run completed the email and CRM steps before the deployment, it reuses those results and runs `track-signup`. The new step must not depend on a later step that has not run yet.

### Change a step ID deliberately

Changing `calculate-risk-score` to `calculate-risk-score-v2` makes an in-progress run execute the new step, even if it completed the old one:

```ts {{ title: "TypeScript" }}
const score = await step.run("calculate-risk-score-v2", () =>
  calculateRiskScoreWithNewModel(user.profile)
);
```

```python {{ title: "Python" }}
async def calculate() -> float:
    return await calculate_risk_score_with_new_model(user.profile)

score = await ctx.step.run("calculate-risk-score-v2", calculate)
```

```go {{ title: "Go" }}
score, err := step.Run(ctx, "calculate-risk-score-v2", func(ctx context.Context) (float64, error) {
	return calculateRiskScoreWithNewModel(ctx, user.Profile)
})
```

If that step writes to another system, the change can repeat a payment, email, or resource creation. Check [Idempotency](/docs-markdown/durable-execution/guides-and-advanced/idempotency) before changing its ID.

## Major logic changes

For a rewrite that cannot use the state of in-progress runs, create a function with a new ID. Keep the original function deployed until its runs finish. Give the functions mutually exclusive [trigger conditions](/docs-markdown/durable-execution/guides-and-advanced/events-and-triggers/event-and-trigger-concepts) so each new event starts only one function:

```ts {{ title: "TypeScript" }}
const CUTOVER_TS = 1704067200000;

export const processUploadV1 = inngest.createFunction(
  {
    id: "process-upload",
    triggers: { event: "file/uploaded", if: `event.ts < ${CUTOVER_TS}` },
  },
  async ({ event, step }) => {
    await step.run("process-file", () => legacyProcessor(event.data.fileId));
  }
);

export const processUploadV2 = inngest.createFunction(
  {
    id: "process-upload-v2",
    triggers: { event: "file/uploaded", if: `event.ts >= ${CUTOVER_TS}` },
  },
  async ({ event, step }) => {
    await step.run("process-file-v2", () => modernProcessor(event.data.fileId));
  }
);
```

```python {{ title: "Python" }}
import inngest

CUTOVER_TS = 1704067200000

@inngest_client.create_function(
    fn_id="process-upload",
    trigger=inngest.TriggerEvent(
        event="file/uploaded", expression=f"event.ts < {CUTOVER_TS}"
    ),
)
async def process_upload_v1(ctx: inngest.Context) -> None:
    async def process_file() -> None:
        await legacy_processor(ctx.event.data["fileId"])

    await ctx.step.run("process-file", process_file)

@inngest_client.create_function(
    fn_id="process-upload-v2",
    trigger=inngest.TriggerEvent(
        event="file/uploaded", expression=f"event.ts >= {CUTOVER_TS}"
    ),
)
async def process_upload_v2(ctx: inngest.Context) -> None:
    async def process_file() -> None:
        await modern_processor(ctx.event.data["fileId"])

    await ctx.step.run("process-file-v2", process_file)
```

```go {{ title: "Go" }}
import (
	"context"
	"fmt"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

const cutoverTS = 1704067200000

type FileUploadedData struct {
	FileID string `json:"fileId"`
}

func ProcessUploadV1(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "process-upload"},
		inngestgo.EventTrigger("file/uploaded", inngestgo.StrPtr(fmt.Sprintf("event.ts < %d", cutoverTS))),
		func(ctx context.Context, input inngestgo.Input[FileUploadedData]) (any, error) {
			_, err := step.Run(ctx, "process-file", func(ctx context.Context) (any, error) {
				return nil, legacyProcessor(ctx, input.Event.Data.FileID)
			})
			return nil, err
		},
	)
}

func ProcessUploadV2(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "process-upload-v2"},
		inngestgo.EventTrigger("file/uploaded", inngestgo.StrPtr(fmt.Sprintf("event.ts >= %d", cutoverTS))),
		func(ctx context.Context, input inngestgo.Input[FileUploadedData]) (any, error) {
			_, err := step.Run(ctx, "process-file-v2", func(ctx context.Context) (any, error) {
				return nil, modernProcessor(ctx, input.Event.Data.FileID)
			})
			return nil, err
		},
	)
}
```

The timestamp filters route new events. Runs that already started under `process-upload` continue there. Once those runs finish, you can remove the original function.

If your producers set the event's [`v` field](/docs-markdown/durable-execution/guides-and-advanced/events-and-triggers/event-payloads-and-schemas), you can route on `event.v` instead. For example, send new events with `v: "2"`, then use `event.v != "2"` for the original function and `event.v == "2"` for the new one.

## Best practices

### Keep IDs stable and distinct

Name each step for its job, such as `charge-customer-payment`. Avoid generic IDs such as `step-1` and IDs built from timestamps or input values. Keep the function ID when a change is compatible with in-progress runs.

### Test a paused run

Use the [Inngest Dev Server](/docs-markdown/durable-execution/guides-and-advanced/testing) to check both paths before deploying:

1. Start a run and pause it with `step.sleep()`.
2. Change the function code, then resume the run. Check which saved results it reuses and which steps it runs.
3. Start a fresh run and confirm that it follows the new path.

Check external side effects and event filters before deploying a change to step IDs or routing.