# Working with loops

> Process lists or paginated data with saved progress so retries resume from completed work.

Process a list or paginated source one item at a time and keep the completed results. Put external calls in named steps so a retry does not repeat work that already succeeded.

## Process a list

This TypeScript v4 example processes each ID in an event. Assume `inngest` and `processItem` are defined by your app.

```typescript {{ title: "TypeScript" }}
import { eventType, staticSchema } from "inngest";

const importRequested = eventType("app/items.import_requested", {
  schema: staticSchema<{ itemIds: string[] }>(),
});

export const importItems = inngest.createFunction(
  { id: "import-items", triggers: [importRequested] },
  async ({ event, step }) => {
    for (const itemId of event.data.itemIds) {
      await step.run("process-item", () => processItem(itemId));
    }
  }
);
```

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

class ImportRequested(pydantic.BaseModel):
    itemIds: list[str]

@inngest_client.create_function(
    fn_id="import-items",
    trigger=inngest.TriggerEvent(event="app/items.import_requested"),
)
async def import_items(ctx: inngest.Context) -> None:
    data = ImportRequested.model_validate(ctx.event.data)

    for item_id in data.itemIds:
        await ctx.step.run("process-item", process_item, item_id)
```

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

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

type ImportRequestedData struct {
	ItemIDs []string `json:"itemIds"`
}

func ImportItems(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "import-items"},
		inngestgo.EventTrigger("app/items.import_requested", nil),
		func(ctx context.Context, input inngestgo.Input[ImportRequestedData]) (any, error) {
			for _, itemID := range input.Event.Data.ItemIDs {
				_, err := step.Run(ctx, "process-item", func(ctx context.Context) (any, error) {
					return processItem(ctx, itemID)
				})
				if err != nil {
					return nil, err
				}
			}
			return nil, nil
		},
	)
}
```

The SDK counts repeated calls to the same step ID, so each iteration has its own saved result. Keep the input list and its order stable while the run is active. If the list comes from an API or database, load it in a step.

## Process a paginated source

When an API provides a cursor, return the next cursor from each step and use it to start the next page. The following example assumes `source.listPage` returns `items` and `nextCursor`, and `store.upsertMany` safely repeats a write.

```typescript {{ title: "TypeScript" }}
export const importPages = inngest.createFunction(
  { id: "import-pages", triggers: { event: "app/pages.import_requested" } },
  async ({ step }) => {
    let cursor: string | null = null;

    do {
      const nextCursor: string | null = await step.run(
        "import-page",
        async () => {
          const page = await source.listPage({ cursor });
          await store.upsertMany(page.items);
          return page.nextCursor;
        }
      );
      cursor = nextCursor;
    } while (cursor !== null);
  }
);
```

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

@inngest_client.create_function(
    fn_id="import-pages",
    trigger=inngest.TriggerEvent(event="app/pages.import_requested"),
)
async def import_pages(ctx: inngest.Context) -> None:
    async def import_page(cursor: str | None) -> str | None:
        page = await source.list_page(cursor=cursor)
        await store.upsert_many(page.items)
        return page.next_cursor

    cursor: str | None = None
    while True:
        cursor = await ctx.step.run("import-page", import_page, cursor)
        if cursor is None:
            break
```

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

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

func ImportPages(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "import-pages"},
		inngestgo.EventTrigger("app/pages.import_requested", nil),
		func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) {
			var cursor *string

			for {
				nextCursor, err := step.Run(ctx, "import-page", func(ctx context.Context) (*string, error) {
					page, err := source.ListPage(ctx, cursor)
					if err != nil {
						return nil, err
					}
					if err := store.UpsertMany(ctx, page.Items); err != nil {
						return nil, err
					}
					return page.NextCursor, nil
				})
				if err != nil {
					return nil, err
				}

				cursor = nextCursor
				if cursor == nil {
					break
				}
			}
			return nil, nil
		},
	)
}
```

A completed iteration saves its cursor. When the function resumes, it re-enters the handler and reads saved step results to reach the next iteration. If a step fails after writing but before it completes, it can repeat the write. Make the external write idempotent.

## Keep loop behavior predictable

- Put API calls, database reads and writes, random values, and current-time decisions inside steps. Code outside steps can run again as the function resumes.
- Keep step IDs and the order of iterations stable for in-progress runs. See [Versioning](/docs-markdown/durable-execution/guides-and-advanced/versioning) when changing a live workflow.
- Use `step.sleep()` between iterations when an external API needs a delay. A sleep pauses the run without holding a worker.
- Keep each step's return value small enough for your plan's limits. Store large payloads outside run state and return an ID or cursor instead. See [Limits](/docs-markdown/durable-execution/limits).
  For many independent items that can run and fail separately, see [Fan-out](/docs-markdown/durable-execution/guides-and-advanced/patterns/fan-out). For parallel steps in one run, use `Promise.all()` (`group.Parallel` in Go, `ctx.group.parallel` in Python) with stable step IDs.

## Related pages

- [Primitives](/docs-markdown/durable-execution/primitives) explains how steps save progress.
- [Idempotency](/docs-markdown/durable-execution/guides-and-advanced/idempotency) explains safe repeated writes.