Sending events from .NET to Inngest
Events start functions. An event has a name and JSON data, and every function with a matching trigger gets a run.
There are two ways to send one: IInngestClient from ordinary application code, and context.Step.SendEventAsync
from inside a function.
Typed events
Section titled “Typed events”An event’s data is usually a record. Implement IInngestEventData on it to give the record the event’s name, and
code can send the record itself:
using Inngest;
public sealed record OrderPlaced(string OrderId, string CustomerId, decimal Total) : IInngestEventData{ public static string EventName => "shop/order.placed";}
public sealed record LabelRequested(string OrderId) : IInngestEventData{ public static string EventName => "shipping/label.requested";
// Optional: the version of the event's schema, sent as "v". public static string? EventVersion => "2026-10-01";}The functions that run on the event name the type as well, with Trigger.Event<OrderPlaced>() or
[EventTrigger<OrderPlaced>]. The name can then
no longer drift from the data it carries, and a typo is a compile error. Everything below also works with an
InngestEvent that names the event as a string, for events without a record of their own.
From application code
Section titled “From application code”AddInngest registers IInngestClient as a singleton. Inject it anywhere:
using Inngest;
public sealed record Order(string Id, string CustomerId, decimal Total);
public sealed class CheckoutService(IInngestClient inngest, ILogger<CheckoutService> logger){ public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken) { // Save the order first, then announce it. If the app dying in between must not lose the event, use the // transactional outbox, which saves the event with the order. var sent = await inngest.SendAsync(new OrderPlaced(order.Id, order.CustomerId, order.Total), cancellationToken);
logger.LogInformation("Order {OrderId} placed as event {EventId}", order.Id, sent.Ids[0]); }}SendAsync returns the ids Inngest gave the events, in the order they were sent. To send several events in one
request, pass a list:
await inngest.SendAsync(orders.Select(order => new OrderPlaced(order.Id, order.CustomerId, order.Total)), cancellationToken);To set an id, a timestamp or user data, wrap the record in an InngestEvent, as the sections below do. Take its name
from the type:
await inngest.SendAsync(new InngestEvent(OrderPlaced.EventName, new OrderPlaced(order.Id, order.CustomerId, order.Total)){ Id = $"order-placed-{order.Id}",}, cancellationToken);From a function
Section titled “From a function”Inside a function, send events with the step tool. The send is a step, so it happens once per run: a replay returns the saved ids instead of sending the events again.
var order = context.Event.Data;
await context.Step.SendEventAsync("request-shipping", new LabelRequested(order.OrderId));The step takes a list of records too, or InngestEvents for events named by a string.
Sending through IInngestClient inside a function would send again on every call that passes that line. Use the
step tool there.
Events sent from a run carry the run’s sessions, and with Inngest.UserContext
the user who caused it.
What an event holds
Section titled “What an event holds”InngestEvent is a record with the name and the data, plus optional properties. A typed send creates one with the
type’s name and version:
| Property | Meaning |
|---|---|
Name |
The event name, such as shop/order.placed. Names that start with inngest/ are reserved and rejected. |
Data |
Any object; serialized with the app’s serializer options, so property names are camel case by default. |
Id |
A deduplication key. Inngest ignores a second event with the same id for 24 hours. |
Timestamp |
When the event happened. Defaults to when Inngest receives it. A time in the future delays the runs it starts. |
User |
Optional user data. |
Version |
An optional version of the event’s schema, sent as v. A typed event takes it from EventVersion. |
Meta |
The sessions the event belongs to. |
Deduplicating with an id
Section titled “Deduplicating with an id”Set Id to something that identifies the occurrence, not the attempt to send it: an order id, a webhook delivery
id, a request id. If the same thing is sent twice, by a retry or a second server, Inngest keeps the first and
starts no new runs for the second. Ids are global across event names, so include the name or a prefix.
The transactional outbox relies on this to send an event again safely.
The Inngest docs explain event ids and deduplication.
Webhooks from Stripe, GitHub, Shopify and others arrive as events whose id is the provider’s id for the delivery, so a delivery that is sent again starts no second run.
Delaying runs
Section titled “Delaying runs”A future Timestamp makes the functions an event triggers start at that time:
await inngest.SendAsync(new InngestEvent("trial/ending-soon", new { userId }){ Timestamp = DateTimeOffset.UtcNow.AddDays(12),}, cancellationToken);Inside a function, sleep instead; see Sleep and wait for events.
Sessions
Section titled “Sessions”Sessions group runs by the work they belong to, such as a support ticket or a conversation, so the Inngest UI can show them together. An event names its sessions as key and id pairs, up to five:
await inngest.SendAsync(new InngestEvent("support/ticket.opened", new { TicketId = "ticket_42" }){ Meta = new EventMeta { Sessions = new Dictionary<string, string?> { ["ticket_id"] = "ticket_42", ["user_id"] = "user_7" }, },});A run’s sessions are those every triggering event belongs to, at most five, in context.Sessions. Events the run
sends, from a send step or through IInngestClient, and functions it invokes inherit them, so the whole chain of
work stays in the session. An event’s own sessions win over inherited ones, and a null id drops an inherited
session; InheritSessions = false sends an event without any. InvokeOptions.Meta does the same for an invocation:
await context.Step.InvokeAsync<string>("summarize", new InvokeOptions("summarize-conversation"){ Meta = new EventMeta { Sessions = new Dictionary<string, string?> { ["user_id"] = null } },});InngestOptions.SessionPropagation = false, or INNGEST_SESSION_PROPAGATION=false, turns inheriting off for the
app; context.Sessions is then empty. A received event’s sessions are in Meta.Sessions, also on the event a wait
matched. Waits do not match on sessions. Middleware sees the inherited sessions in Meta.PropagatedSessions and may
remove some before an event is sent.
Keys and modes
Section titled “Keys and modes”In production the client needs an event key: set INNGEST_EVENT_KEY or InngestOptions.EventKey. Without one,
SendAsync throws an InngestException. In dev mode no key is needed; the dev server accepts any. See
Configuration.
Failures and retries
Section titled “Failures and retries”The client retries a send that fails with a network error, a timeout, a 5xx, 408, 425 or 429, up to five
attempts, waiting 100 ms, 200 ms, 400 ms and so on with some jitter in between. Other errors, such as 401 for a
wrong key or 400 for an invalid event, fail at once.
Every attempt of one SendAsync call carries the same seed for event ids, so a retry after a lost response creates
the same events rather than copies.
When the send fails for good, SendAsync throws InngestException, with the HTTP status in StatusCode when
there was one. Inside a function, SendEventAsync fails like any other step and is retried with it.
try{ await inngest.SendAsync(new InngestEvent("shop/cart.abandoned", new { cartId = "c_42" }), cancellationToken);}catch (InngestException exception) when (exception.StatusCode is 401){ logger.LogError(exception, "The Inngest event key is wrong or missing."); throw;}Changing events in one place
Section titled “Changing events in one place”Middleware can see and change every event the app sends, from code and from functions, for example to add
tracing data or a tenant id. See WrapSendEventAsync in Middleware.
Inngest limits the size of an event and the number of events per request; the limits page lists them.
Related examples
Section titled “Related examples”Nightly billing fans out with SendEventAsync, and the webhook pipeline sends one event per delivery.
inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.