Skip to content

How Inngest runs .NET functions

Inngest runs a durable function by calling your app once per step. Every call runs the function from the top, and the steps that already finished return their saved results, so the function picks up where it stopped.

Inngestsavedchargesend-receiptcooling-off
CheckoutFunction.csCall 4 of 5
saved resultvar charge = await context.Step.RunAsync("charge",    ct => pay.ChargeAsync(order, ct));saved resultawait context.Step.RunAsync("send-receipt",    ct => mail.SendReceiptAsync(order, ct));saved resultawait context.Step.SleepAsync("cooling-off",    TimeSpan.FromMinutes(30));runningawait context.Step.RunAsync("ship",    ct => warehouse.ShipAsync(order, ct));not reachedreturn new { charge.Id };

Call 4. Thirty minutes later, every finished step returns its saved result, and ship runs.

  1. Nothing is saved yet, so charge runs. The app answers 206 with its result, and Inngest saves it.
  2. charge returns its saved result at once, without charging again, and send-receipt runs.
  3. Both finished steps return their saved results. The sleep is new: the app reports it, and Inngest calls again in 30 minutes.
  4. Thirty minutes later, every finished step returns its saved result, and ship runs.
  5. Every step returns its saved result, the function returns, and the app answers 200. The run is complete.
One run of CheckoutFunction, call by call. Inngest calls the app's /api/inngest endpoint, or a Connect worker, with the saved results of the finished steps. The function runs from the top, the finished steps return their saved results at once, and the next step runs. The app answers 206 with that step's result, and 200 when the function returns.
  1. Inngest calls your app. It sends a request to /api/inngest, or to a Connect worker, with the saved result of every finished step.

  2. The function runs from the top. A step that finished in an earlier call returns its saved result at once. It does not run again.

  3. The next step runs. The first step without a saved result runs, or the run reports a sleep or a wait.

  4. The app answers. 206 with the step’s result, which Inngest saves before it calls again, now or when the sleep is over. 200 with the output once the function returns.

One run over HTTP, animated. An event reaches Inngest, which calls the app once per step: charge answers 206 with its result, send-receipt fails and is retried after a backoff, and the last call answers 200 with the output.One run over HTTP, animated. An event reaches Inngest, which calls the app once per step: charge answers 206 with its result, send-receipt fails and is retried after a backoff, and the last call answers 200 with the output.

Event source

Your API, a webhook or a schedule

Inngest

Inngest Cloud, self-hosted or the dev server

Step state stored by Inngest
  • charge done
  • send-receipt done

Over HTTP

Your ASP.NET Core app

MapInngest runs one step per call

Or over Connect, with no public endpoint

Your Connect worker

No public endpoint; runs behind a firewall

Your code sends the events, Inngest stores the run and decides when to call, and the app keeps no state between calls, so any instance can take any call.

One run of FulfillOrder, call by call: what Inngest sent, which steps had saved results, and what the SDK answered. The answers are what the SDK in this repository returns; the site’s snippet check fails if they ever stop matching.

Call 1 of 7: The first call

Inngest calls the function for the first time, with no memoized steps. The function runs until reserve-stock. That is the only new step and a run step, so the SDK runs it at once and answers 206 with a StepRun op that carries the result.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (ran in this call)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card",            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30));        await Task.WhenAll(            context.Step.RunAsync("send-receipt",                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order",                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId);    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=step

Memoized steps (0)

None yet: steps is an empty object.

ctx.stack lists 0 steps in the order they completed; disable_immediate_execution is false.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {},
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [],
      "current": 0
    },
    "disable_immediate_execution": false,
    "use_api": false
  },
  "version": 2
}

The SDK answers

206One op for Inngest to act on.

  • StepRunreserve-stockdata: {"reservationId":"res_8c1f"}
Full response body
[
  {
    "id": "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935",
    "op": "StepRun",
    "name": "reserve-stock",
    "displayName": "reserve-stock",
    "opts": {
      "type": "step.run"
    },
    "userland": {
      "id": "reserve-stock"
    },
    "data": {
      "reservationId": "res_8c1f"
    },
    "timing": {
      "a": 1793611800000000000,
      "b": 142000000
    }
  }
]

Call 2 of 7: Replaying a memoized step

Inngest saved the reservation and calls again. The SDK runs the function from the top, and reserve-stock returns its saved result without calling the inventory service. The function reaches charge-card, which runs the same way.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (memoized)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card", (ran in this call)            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30));        await Task.WhenAll(            context.Step.RunAsync("send-receipt",                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order",                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId);    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=step

Memoized steps (1)

  • reserve-stockc3c77fb…{"data":{"reservationId":"res_8c1f"}}

ctx.stack lists 1 step in the order they completed; disable_immediate_execution is false.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {
    "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935": {
      "data": {
        "reservationId": "res_8c1f"
      }
    }
  },
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [
        "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935"
      ],
      "current": 1
    },
    "disable_immediate_execution": false,
    "use_api": false
  },
  "version": 2
}

The SDK answers

206One op for Inngest to act on.

  • StepRuncharge-carddata: {"paymentId":"pay_31d9","amount":59.9}
Full response body
[
  {
    "id": "b2ae8f272ea660900fca3aeefbd39695c43f5cc9",
    "op": "StepRun",
    "name": "charge-card",
    "displayName": "charge-card",
    "opts": {
      "type": "step.run"
    },
    "userland": {
      "id": "charge-card"
    },
    "data": {
      "paymentId": "pay_31d9",
      "amount": 59.9
    },
    "timing": {
      "a": 1793611800142000000,
      "b": 418000000
    }
  }
]

Call 3 of 7: A sleep ends the call

With two results memoized, the function reaches cooling-off. Inngest carries out sleeps, so the SDK answers with a Sleep op and the call ends. Nothing in the app waits for the 30 minutes.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (memoized)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card", (memoized)            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30)); (sent to Inngest)        await Task.WhenAll(            context.Step.RunAsync("send-receipt",                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order",                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId);    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=step

Memoized steps (2)

  • reserve-stockc3c77fb…{"data":{"reservationId":"res_8c1f"}}
  • charge-cardb2ae8f2…{"data":{"paymentId":"pay_31d9","amount":59.9}}

ctx.stack lists 2 steps in the order they completed; disable_immediate_execution is false.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {
    "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935": {
      "data": {
        "reservationId": "res_8c1f"
      }
    },
    "b2ae8f272ea660900fca3aeefbd39695c43f5cc9": {
      "data": {
        "paymentId": "pay_31d9",
        "amount": 59.9
      }
    }
  },
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [
        "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935",
        "b2ae8f272ea660900fca3aeefbd39695c43f5cc9"
      ],
      "current": 2
    },
    "disable_immediate_execution": false,
    "use_api": false
  },
  "version": 2
}

The SDK answers

206One op for Inngest to act on.

  • Sleepcooling-offduration: 30m
Full response body
[
  {
    "id": "d6ef22dea68b364acf8b96e9fa39dcb222867fdf",
    "op": "Sleep",
    "name": "30m",
    "displayName": "cooling-off",
    "opts": {
      "duration": "30m"
    },
    "userland": {
      "id": "cooling-off"
    }
  }
]

Call 4 of 7: Two steps at once

Thirty minutes later Inngest memoizes the sleep as null and calls again. The function starts send-receipt and ship-order together and awaits Task.WhenAll. There are two new steps, so the SDK runs neither and answers with two StepPlanned ops.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (memoized)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card", (memoized)            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30)); (memoized)        await Task.WhenAll(            context.Step.RunAsync("send-receipt", (planned)                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order", (planned)                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId);    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=step

Memoized steps (3)

  • reserve-stockc3c77fb…{"data":{"reservationId":"res_8c1f"}}
  • charge-cardb2ae8f2…{"data":{"paymentId":"pay_31d9","amount":59.9}}
  • cooling-offd6ef22d…null

ctx.stack lists 3 steps in the order they completed; disable_immediate_execution is false.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {
    "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935": {
      "data": {
        "reservationId": "res_8c1f"
      }
    },
    "b2ae8f272ea660900fca3aeefbd39695c43f5cc9": {
      "data": {
        "paymentId": "pay_31d9",
        "amount": 59.9
      }
    },
    "d6ef22dea68b364acf8b96e9fa39dcb222867fdf": null
  },
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [
        "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935",
        "b2ae8f272ea660900fca3aeefbd39695c43f5cc9",
        "d6ef22dea68b364acf8b96e9fa39dcb222867fdf"
      ],
      "current": 3
    },
    "disable_immediate_execution": false,
    "use_api": false
  },
  "version": 2
}

The SDK answers

2062 ops for Inngest to act on.

  • StepPlannedsend-receipt
  • StepPlannedship-order
Full response body
[
  {
    "id": "179d7057db2cb69316c43e700089e6aeec07caff",
    "op": "StepPlanned",
    "name": "send-receipt",
    "displayName": "send-receipt",
    "opts": {
      "type": "step.run"
    },
    "userland": {
      "id": "send-receipt"
    }
  },
  {
    "id": "8b4b44df020db368ad1ab221cf78db64d9d78e13",
    "op": "StepPlanned",
    "name": "ship-order",
    "displayName": "ship-order",
    "opts": {
      "type": "step.run"
    },
    "userland": {
      "id": "ship-order"
    }
  }
]

Call 5 of 7: One planned step per call

Inngest runs each planned step in a call of its own, and makes both calls at the same time. This one has stepId set to the hash of send-receipt. The SDK replays up to the parallel section, runs only that step and answers with its result.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (memoized)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card", (memoized)            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30)); (memoized)        await Task.WhenAll(            context.Step.RunAsync("send-receipt", (ran in this call)                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order",                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId);    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=179d705…

stepId is the hash of send-receipt, so the SDK runs only that step.

Memoized steps (3)

  • reserve-stockc3c77fb…{"data":{"reservationId":"res_8c1f"}}
  • charge-cardb2ae8f2…{"data":{"paymentId":"pay_31d9","amount":59.9}}
  • cooling-offd6ef22d…null

ctx.stack lists 3 steps in the order they completed; disable_immediate_execution is true.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {
    "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935": {
      "data": {
        "reservationId": "res_8c1f"
      }
    },
    "b2ae8f272ea660900fca3aeefbd39695c43f5cc9": {
      "data": {
        "paymentId": "pay_31d9",
        "amount": 59.9
      }
    },
    "d6ef22dea68b364acf8b96e9fa39dcb222867fdf": null
  },
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [
        "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935",
        "b2ae8f272ea660900fca3aeefbd39695c43f5cc9",
        "d6ef22dea68b364acf8b96e9fa39dcb222867fdf"
      ],
      "current": 3
    },
    "disable_immediate_execution": true,
    "use_api": false
  },
  "version": 2
}

The SDK answers

206One op for Inngest to act on.

  • StepRunsend-receiptdata: null
Full response body
[
  {
    "id": "179d7057db2cb69316c43e700089e6aeec07caff",
    "op": "StepRun",
    "name": "send-receipt",
    "displayName": "send-receipt",
    "opts": {
      "type": "step.run"
    },
    "userland": {
      "id": "send-receipt"
    },
    "data": null,
    "timing": {
      "a": 1793613600560000000,
      "b": 236000000
    }
  }
]

Call 6 of 7: The other planned step

The same for ship-order, in a parallel call that carries the same memoized steps. disable_immediate_execution is true while the parallel steps run.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (memoized)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card", (memoized)            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30)); (memoized)        await Task.WhenAll(            context.Step.RunAsync("send-receipt",                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order", (ran in this call)                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId);    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=8b4b44d…

stepId is the hash of ship-order, so the SDK runs only that step.

Memoized steps (3)

  • reserve-stockc3c77fb…{"data":{"reservationId":"res_8c1f"}}
  • charge-cardb2ae8f2…{"data":{"paymentId":"pay_31d9","amount":59.9}}
  • cooling-offd6ef22d…null

ctx.stack lists 3 steps in the order they completed; disable_immediate_execution is true.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {
    "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935": {
      "data": {
        "reservationId": "res_8c1f"
      }
    },
    "b2ae8f272ea660900fca3aeefbd39695c43f5cc9": {
      "data": {
        "paymentId": "pay_31d9",
        "amount": 59.9
      }
    },
    "d6ef22dea68b364acf8b96e9fa39dcb222867fdf": null
  },
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [
        "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935",
        "b2ae8f272ea660900fca3aeefbd39695c43f5cc9",
        "d6ef22dea68b364acf8b96e9fa39dcb222867fdf"
      ],
      "current": 3
    },
    "disable_immediate_execution": true,
    "use_api": false
  },
  "version": 2
}

The SDK answers

206One op for Inngest to act on.

  • StepRunship-orderdata: "trk_5b72"
Full response body
[
  {
    "id": "8b4b44df020db368ad1ab221cf78db64d9d78e13",
    "op": "StepRun",
    "name": "ship-order",
    "displayName": "ship-order",
    "opts": {
      "type": "step.run"
    },
    "userland": {
      "id": "ship-order"
    },
    "data": "trk_5b72",
    "timing": {
      "a": 1793613600796000000,
      "b": 307000000
    }
  }
]

Call 7 of 7: The function returns

Once both parallel steps are memoized, Inngest calls once more. Every step returns its saved result, the function returns OrderFulfilled, and the SDK answers 200 with the output. The run is complete.

FulfillOrder.cs
public sealed class FulfillOrder(    IInventory inventory, IPayments payments, IMailer mailer, IWarehouse warehouse)    : InngestFunction<OrderPlaced>{    public override async Task<object?> ExecuteAsync(        FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)    {        var order = context.Event.Data;        var reservation = await context.Step.RunAsync("reserve-stock", (memoized)            ct => inventory.ReserveAsync(order.OrderId, ct));        var payment = await context.Step.RunAsync("charge-card", (memoized)            ct => payments.ChargeAsync(order.CustomerId, order.Total, ct));        await context.Step.SleepAsync("cooling-off", TimeSpan.FromMinutes(30)); (memoized)        await Task.WhenAll(            context.Step.RunAsync("send-receipt", (memoized)                ct => mailer.SendReceiptAsync(order.OrderId, payment.PaymentId, ct)),            context.Step.RunAsync("ship-order", (memoized)                ct => warehouse.ShipAsync(reservation.ReservationId, ct)));        return new OrderFulfilled(order.OrderId, payment.PaymentId); (returned)    }}

Request from Inngest

POST /api/inngest?fnId=shop-fulfill-order&stepId=step

Memoized steps (5)

  • reserve-stockc3c77fb…{"data":{"reservationId":"res_8c1f"}}
  • charge-cardb2ae8f2…{"data":{"paymentId":"pay_31d9","amount":59.9}}
  • cooling-offd6ef22d…null
  • send-receipt179d705…{"data":null}
  • ship-order8b4b44d…{"data":"trk_5b72"}

ctx.stack lists 5 steps in the order they completed; disable_immediate_execution is false.

Full request body
{
  "event": {
    "name": "shop/order.placed",
    "data": {
      "orderId": "ord_1042",
      "customerId": "cus_7d2e",
      "total": 59.9
    },
    "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
    "ts": 1793611798000,
    "user": {}
  },
  "events": [
    {
      "name": "shop/order.placed",
      "data": {
        "orderId": "ord_1042",
        "customerId": "cus_7d2e",
        "total": 59.9
      },
      "id": "01JDZ6Q2XF4H9K1M3N5P7R9T2V",
      "ts": 1793611798000,
      "user": {}
    }
  ],
  "steps": {
    "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935": {
      "data": {
        "reservationId": "res_8c1f"
      }
    },
    "b2ae8f272ea660900fca3aeefbd39695c43f5cc9": {
      "data": {
        "paymentId": "pay_31d9",
        "amount": 59.9
      }
    },
    "d6ef22dea68b364acf8b96e9fa39dcb222867fdf": null,
    "179d7057db2cb69316c43e700089e6aeec07caff": {
      "data": null
    },
    "8b4b44df020db368ad1ab221cf78db64d9d78e13": {
      "data": "trk_5b72"
    }
  },
  "ctx": {
    "run_id": "01JDZ6Q3T8K3V2M4H5N6P7R8S9",
    "attempt": 0,
    "max_attempts": 4,
    "stack": {
      "stack": [
        "c3c77fbce7bd0d6d6ed264a1e6030ce7955ef935",
        "b2ae8f272ea660900fca3aeefbd39695c43f5cc9",
        "d6ef22dea68b364acf8b96e9fa39dcb222867fdf",
        "179d7057db2cb69316c43e700089e6aeec07caff",
        "8b4b44df020db368ad1ab221cf78db64d9d78e13"
      ],
      "current": 5
    },
    "disable_immediate_execution": false,
    "use_api": false
  },
  "version": 2
}

The SDK answers

200The function returned; the body is its output.

Full response body
{
  "orderId": "ord_1042",
  "paymentId": "pay_31d9"
}
The function The SDK answers
reaches one new RunAsync step runs it and answers 206 with a StepRun op and its result
starts several new steps together runs none and answers 206 with a StepPlanned op for each; Inngest runs each in its own call
reaches a sleep, a wait for an event or an invocation answers 206 with that op; Inngest carries it out
returns answers 200 with the output
throws answers 500 if Inngest should retry, 400 if the failure is final

With streaming on, a call that runs longer than three seconds is answered with a 201 at once, and the same answer follows inside the stream when the call ends. The design document lists every field.

  • Code outside steps runs on every call. Side effects and anything that differs between calls, such as the clock or a new id, belong in a step. See Steps.
  • Step ids identify results. Keep them the same while runs are in flight.
  • Results are JSON. Return types that round-trip through System.Text.Json, such as records.
  • Nothing waits in your app. A week-long sleep is a timestamp in Inngest; deploys and restarts do not affect it.

inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.