# Working with Loops in Inngest

In Inngest each step in your function is executed as a separate HTTP request.  This means that for every step in your function, the function is re-entered, starting from the beginning, up to the point where the next step is executed. This [execution model](/docs-markdown/learn/how-functions-are-executed) helps in managing retries, timeouts, and ensures robustness in distributed systems.

This page covers how to implement loops in your Inngest functions and avoid common pitfalls.

## Simple function example

Let's start with a simple example to illustrate the concept:

```javascript
inngest.createFunction(
  { id: "simple-function", triggers: { event: "test/simple.function" } },
  async ({ step }) => {
    console.log("hello");

    await step.run("a", async () => { console.log("a") });
    await step.run("b", async () => { console.log("b") });
    await step.run("c", async () => { console.log("c") });
  }
);
```

In the above example, you will see "hello" printed four times, once for the initial function entry and once for each step execution (`a`, `b`, and `c`).

```bash {{ title: "✅ How Inngest executes the code" }}

# This is how Inngest executes the code above:

<run start>
"hello"

"hello"
"a"

"hello"
"b"

"hello"
"c"
<run complete>
```

```bash {{ title: "❌ Common incorrect misconception" }}

# This is a common assumption of how Inngest executes the code above.
# It is not correct.

<run start>

"hello"
"a"
"b"
"c"

<run complete>
```

Any non-deterministic logic (like database calls or API calls) must be placed inside a `step.run` call to ensure it is executed correctly within each step.

With this in mind, here is how the previous example can be fixed:

```ts
inngest.createFunction(
  { id: "simple-function", triggers: { event: "test/simple.function" } },
  async ({ step }) => {
    await step.run("hello", () => { console.log("hello") });

    await step.run("a", async () => { console.log("a") });
    await step.run("b", async () => { console.log("b") });
    await step.run("c", async () => { console.log("c") });
  }
);

// hello
// a
// b
// c
```

Now, "hello" is printed only once, as expected.

Let's start with a simple example to illustrate the concept:

```go
import (
    "fmt"
    "github.com/inngest/inngestgo"
    "github.com/inngest/inngestgo/step"
)

inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{ID: "simple-function"},
	inngestgo.EventTrigger("test/simple.function", nil),
	func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) {
		fmt.Println("hello")

		_, err := step.Run(ctx, "a", func(ctx context.Context) (any, error) {
			fmt.Println("a")
			return nil, nil
		})
		if err != nil {
			return nil, err
		}

		_, err = step.Run(ctx, "b", func(ctx context.Context) (any, error) {
			fmt.Println("b")
			return nil, nil
		})
		if err != nil {
			return nil, err
		}

		_, err = step.Run(ctx, "c", func(ctx context.Context) (any, error) {
			fmt.Println("c")
			return nil, nil
		})
		if err != nil {
			return nil, err
		}

		return nil, nil
	},
)
```

In the above example, you will see "hello" printed four times, once for the initial function entry and once for each step execution (`a`, `b`, and `c`).

```bash {{ title: "✅ How Inngest executes the code" }}

# This is how Inngest executes the code above:

<run start>
"hello"

"hello"
"a"

"hello"
"b"

"hello"
"c"
<run complete>
```

```bash {{ title: "❌ Common incorrect misconception" }}

# This is a common assumption of how Inngest executes the code above.
# It is not correct.

<run start>

"hello"
"a"
"b"
"c"

<run complete>
```

Any non-deterministic logic (like database calls or API calls) must be placed inside a `step.run` call to ensure it is executed correctly within each step.

With this in mind, here is how the previous example can be fixed:

```go
import (
    "fmt"
    "github.com/inngest/inngestgo"
    "github.com/inngest/inngestgo/step"
)

inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{ID: "simple-function"},
	inngestgo.EventTrigger("test/simple.function", nil),
	func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) {
		if _, err := step.Run(ctx, "hello", func(ctx context.Context) (any, error) {
			fmt.Println("hello")
			return nil, nil
		}); err != nil {
			return nil, err
		}

		if _, err := step.Run(ctx, "a", func(ctx context.Context) (any, error) {
			fmt.Println("a")
			return nil, nil
		}); err != nil {
			return nil, err
		}

		if _, err := step.Run(ctx, "b", func(ctx context.Context) (any, error) {
			fmt.Println("b")
			return nil, nil
		}); err != nil {
			return nil, err
		}

		if _, err := step.Run(ctx, "c", func(ctx context.Context) (any, error) {
			fmt.Println("c")
			return nil, nil
		}); err != nil {
			return nil, err
		}

		return nil, nil
	},
)

// hello
// a 
// b
// c
```

Now, "hello" is printed only once, as expected.

Let's start with a simple example to illustrate the concept:

```python
@inngest_client.create_function(
    fn_id="simple-function",
    trigger=inngest.TriggerEvent(event="test/simple.function")
)
async def simple_function(ctx: inngest.Context):
    print("hello")

    async def step_a():
        print("a")
    await ctx.step.run("a", step_a)

    async def step_b():
        print("b") 
    await ctx.step.run("b", step_b)

    async def step_c():
        print("c")
    await ctx.step.run("c", step_c)
```

In the above example, you will see "hello" printed four times, once for the initial function entry and once for each step execution (`a`, `b`, and `c`).

```bash {{ title: "✅ How Inngest executes the code" }}

# This is how Inngest executes the code above:

<run start>
"hello"

"hello"
"a"

"hello"
"b"

"hello"
"c"
<run complete>
```

```bash {{ title: "❌ Common incorrect misconception" }}

# This is a common assumption of how Inngest executes the code above.
# It is not correct.

<run start>

"hello"
"a"
"b"
"c"

<run complete>
```

Any non-deterministic logic (like database calls or API calls) must be placed inside a `step.run` call to ensure it is executed correctly within each step.

With this in mind, here is how the previous example can be fixed:

```python
import inngest
from src.inngest.client import inngest_client

@inngest_client.create_function(
    id="simple-function",
    trigger=inngest.TriggerEvent(event="test/simple.function")
)
async def simple_function(ctx: inngest.Context):
    await ctx.step.run("hello", lambda: print("hello"))

    await ctx.step.run("a", lambda: print("a"))
    await ctx.step.run("b", lambda: print("b")) 
    await ctx.step.run("c", lambda: print("c"))

# hello
# a
# b
# c
```

Now, "hello" is printed only once, as expected.

## Loop example

Here's [an example](/blog/import-ecommerce-api-data-in-seconds) of an Inngest function that imports all products from a Shopify store into a local system. This function iterates over all pages combining all products into a single array.

```typescript
export default inngest.createFunction(
  { id: "shopify-product-import", triggers: { event: "shopify/import.requested" } },
  async ({ event, step }) => {
    const allProducts = []
    let cursor = null
    let hasMore = true

    // Use the event's "data" to pass key info like IDs
    // Note: in this example is deterministic across multiple requests
    // If the returned results must stay in the same order, wrap the db call in step.run()
    const session = await database.getShopifySession(event.data.storeId)

    while (hasMore) {
      const page = await step.run(`fetch-products-${pageNumber}`, async () => {
        return await shopify.rest.Product.all({
          session,
          since_id: cursor,
        })
      })
      // Combine all of the data into a single list
      allProducts.push(...page.products)
      if (page.products.length === 50) {
        cursor = page.products[49].id
      } else {
        hasMore = false
      }
    }

    // Now we have the entire list of products within allProducts!
  }
)
```

In the example above, each iteration of the loop is managed using `step.run()`, ensuring that **all non-deterministic logic (like fetching products from Shopify) is encapsulated within a step**. This approach guarantees that if the request fails, it will be retried automatically, in the correct order. This structure aligns with Inngest's execution model, where each step is a separate HTTP request, ensuring robust and consistent loop behavior.

Note that in the example above `getShopifySession` is deterministic across multiple requests (and it's added to all API calls for authorization). If the returned results must stay in the same order, wrap the database call in `step.run()`.

Read more about this use case in the [blog post](/blog/import-ecommerce-api-data-in-seconds).

Here's an example of an Inngest function that imports all products from a Shopify store into a local system. This function iterates over all pages combining all products into a single array.

```go
_, err := inngestgo.CreateFunction(
	client,
	inngestgo.FunctionOpts{
		Name: "shopify-product-import",
	},
	inngestgo.EventTrigger("shopify/import.requested", nil),
	func(ctx context.Context, input inngestgo.Input[map[string]any]) (any, error) {
		var allProducts []shopify.Product
		var cursor *string
		hasMore := true

		// Use the event's "data" to pass key info like IDs
		// Note: in this example is deterministic across multiple requests
		// If the returned results must stay in the same order, wrap the db call in step.run()
		session, err := database.GetShopifySession(input.Event.Data["storeId"].(string))
		if err != nil {
			return err
		}

		for hasMore {
			if page, err := step.Run(ctx, fmt.Sprintf("fetch-products-%v", cursor), func(ctx context.Context) ([]shopify.Product, error) {
				return shopify.Product.All(&shopify.ProductListOptions{
					Session: session,
					SinceID: cursor,
				})
			}); err != nil {
				return err, nil
			}

			// Combine all of the data into a single list
			allProducts = append(allProducts, page.Products...)

			if len(page.Products) == 50 {
				id := page.Products[49].ID
				cursor = &id
			} else {
				hasMore = false
			}
		}

		// Now we have the entire list of products within allProducts!
		return nil, nil
	},
)
```

In the example above, each iteration of the loop is managed using `step.Run()`, ensuring that **all non-deterministic logic (like fetching products from Shopify) is encapsulated within a step**. This approach guarantees that if the request fails, it will be retried automatically, in the correct order. This structure aligns with Inngest's execution model, where each step is a separate HTTP request, ensuring robust and consistent loop behavior.

Note that in the example above `getShopifySession` is deterministic across multiple requests (and it's added to all API calls for authorization). If the returned results must stay in the same order, wrap the database call in `step.Run()`.

Read more about this use case in the [blog post](/blog/import-ecommerce-api-data-in-seconds).

Here's an example of an Inngest function that imports all products from a Shopify store into a local system. This function iterates over all pages combining all products into a single array.

```python
@inngest.create_function(
    id="shopify-product-import",
    trigger=inngest.TriggerEvent(event="shopify/import.requested")
)
async def shopify_product_import(ctx: inngest.Context):
    all_products = []
    cursor = None
    has_more = True

    # Use the event's "data" to pass key info like IDs
    # Note: in this example is deterministic across multiple requests
    # If the returned results must stay in the same order, wrap the db call in step.run()
    session = await database.get_shopify_session(ctx.event.data["store_id"])

    while has_more:
        page = await ctx.step.run(f"fetch-products-{cursor}", lambda: shopify.Product.all(
            session=session,
            since_id=cursor
        ))
        # Combine all of the data into a single list
        all_products.extend(page.products)
        if len(page.products) == 50:
            cursor = page.products[49].id
        else:
            has_more = False

    # Now we have the entire list of products within all_products!
```

In the example above, each iteration of the loop is managed using `step.run()`, ensuring that **all non-deterministic logic (like fetching products from Shopify) is encapsulated within a step**. This approach guarantees that if the request fails, it will be retried automatically, in the correct order. This structure aligns with Inngest's execution model, where each step is a separate HTTP request, ensuring robust and consistent loop behavior.

Note that in the example above `get_shopify_session` is deterministic across multiple requests (and it's added to all API calls for authorization). If the returned results must stay in the same order, wrap the database call in `step.run()`.

Read more about this use case in the [blog post](/blog/import-ecommerce-api-data-in-seconds).

## Best practices: implementing loops in Inngest

To ensure your loops run correctly within [Inngest's execution model](/docs-markdown/learn/how-functions-are-executed):

### 1. Treat each loop iterations as a single step

In a typical programming environment, loops maintain their state across iterations. In Inngest, each step re-executes the function from the beginning to ensure that only the failed steps will be re-tried. To handle this, treat each loop iteration as a separate step. This way, the loop progresses correctly, and each iteration builds on the previous one.

### 2. Place non-deterministic logic inside steps

Place non-deterministic logic (like API calls, database queries, or random number generation) inside `step.run` calls. This ensures that such operations are executed correctly and consistently within each step, preventing repeated execution with each function re-entry.

### 3. Use sleep effectively

When using `step.sleep` inside a loop, ensure it is combined with structuring the loop to handle each iteration as a separate step. This prevents the function from appearing to restart and allows for controlled timing between iterations.

## Next steps

- Docs explanation: [Inngest execution model](/docs-markdown/learn/how-functions-are-executed).
- Docs guide: [multi-step functions](/docs-markdown/guides/multi-step-functions).
- Blog post: ["How to import 1000s of items from any E-commerce API in seconds with serverless functions"](/blog/import-ecommerce-api-data-in-seconds).