A .NET SDK for Inngest, a platform for building reliable, scalable event-driven workflows.
- Attribute-based functions: Define functions using familiar .NET patterns with attributes
- Full dependency injection: Constructor injection with scoped services per function invocation
- Durable execution: Steps automatically retry and resume from failures
- Step primitives: Run, Sleep, SleepUntil, WaitForEvent, Invoke, SendEvent
- Flow control: Concurrency, rate limiting, throttling, debounce, batching
- Full observability: Built-in logging with ILogger support
dotnet add package Inngest.NETusing Inngest;
var builder = WebApplication.CreateBuilder(args);
// Add Inngest with configuration and auto-discover functions
builder.Services
.AddInngest(options =>
{
options.AppId = "my-app";
options.IsDev = true; // Use Inngest Dev Server
})
.AddFunctionsFromAssembly(typeof(Program).Assembly);
var app = builder.Build();
// Mount the Inngest endpoint
app.UseInngest("/api/inngest");
app.Run();using Inngest;
using Inngest.Attributes;
[InngestFunction("process-order", Name = "Process Order")]
[EventTrigger("shop/order.created")]
[Retry(Attempts = 5)]
public class OrderProcessor : IInngestFunction
{
private readonly IOrderService _orderService;
private readonly ILogger<OrderProcessor> _logger;
// Constructor injection - services are scoped per function invocation
public OrderProcessor(IOrderService orderService, ILogger<OrderProcessor> logger)
{
_orderService = orderService;
_logger = logger;
}
public async Task<object?> ExecuteAsync(InngestContext context, CancellationToken cancellationToken)
{
// Step 1: Validate order (memoized - runs once, replays on retries)
var order = await context.Step.Run("validate-order", async () =>
{
_logger.LogInformation("Validating order");
return await _orderService.ValidateAsync(context.Event.Data);
});
// Step 2: Sleep for 5 minutes (durable - survives restarts)
await context.Step.Sleep("wait-for-processing", TimeSpan.FromMinutes(5));
// Step 3: Process payment
var payment = await context.Step.Run("process-payment", async () =>
{
return await _orderService.ProcessPaymentAsync(order.Id);
});
return new { status = "completed", orderId = order.Id };
}
}Define events that know their own name using IInngestEventData:
// Event definition - the event name lives with the event type
public record OrderCreatedEvent : IInngestEventData
{
public static string EventName => "shop/order.created";
public required string OrderId { get; init; }
public required decimal Amount { get; init; }
public required string CustomerId { get; init; }
}
// Function - NO [EventTrigger] needed! Trigger auto-derived from OrderCreatedEvent.EventName
[InngestFunction("order-handler", Name = "Order Handler")]
public class OrderHandler : IInngestFunction<OrderCreatedEvent>
{
public async Task<object?> ExecuteAsync(
InngestContext<OrderCreatedEvent> context,
CancellationToken cancellationToken)
{
// Fully typed event data - no nulls, no magic strings
var eventData = context.Event.Data!;
await context.Step.Run("process", () =>
{
Console.WriteLine($"Order {eventData.OrderId} for ${eventData.Amount}");
return true;
});
return new { processed = true };
}
}
// Sending events - type-safe, event name derived automatically
await inngestClient.SendAsync(new OrderCreatedEvent
{
OrderId = "123",
Amount = 99.99m,
CustomerId = "cust-456"
});Marks a class as an Inngest function:
[InngestFunction("my-function-id", Name = "Human Readable Name")]Triggers the function when a specific event is received:
[EventTrigger("user/signed.up")]
[EventTrigger("user/invited", Expression = "event.data.role == 'admin'")] // With filterTriggers the function on a schedule:
[CronTrigger("0 0 * * *")] // Every day at midnight
[CronTrigger("*/30 * * * *")] // Every 30 minutesConfigures retry behavior:
[Retry(Attempts = 5)]Limits concurrent executions:
[Concurrency(5)] // Max 5 concurrent executions
[Concurrency(1, Key = "event.data.userId")] // Per-user concurrencyLimits execution rate:
[RateLimit(100, Period = "1h")] // 100 per hour
[RateLimit(10, Period = "1m", Key = "event.data.customerId")] // Per-customer// appsettings.json
{
"Inngest": {
"AppId": "my-app",
"EventKey": "your-event-key",
"SigningKey": "your-signing-key",
"DisableCronTriggersInDev": true // Optional: prevent cron jobs locally
}
}
// Program.cs
builder.Services
.AddInngest(builder.Configuration.GetSection("Inngest"))
.AddFunctionsFromAssembly(typeof(Program).Assembly);builder.Services
.AddInngest(options =>
{
options.AppId = "my-app";
options.EventKey = "your-event-key";
options.SigningKey = "your-signing-key";
options.IsDev = builder.Environment.IsDevelopment();
options.DisableCronTriggersInDev = true; // Prevent cron jobs from running locally
})
.AddFunction<OrderProcessor>()
.AddFunction<EmailSender>();| Variable | Description |
|---|---|
INNGEST_EVENT_KEY |
Your Inngest event key for sending events |
INNGEST_SIGNING_KEY |
Your Inngest signing key for authentication |
INNGEST_SIGNING_KEY_FALLBACK |
Optional fallback signing key |
INNGEST_ENV |
Environment name (e.g., "production", "staging") |
INNGEST_DEV |
Set to any value or URL to use Inngest Dev Server |
INNGEST_DISABLE_CRON_IN_DEV |
Set to true or 1 to disable cron triggers in dev mode |
INNGEST_SERVE_ORIGIN |
Base URL for your application |
INNGEST_SERVE_PATH |
Path for the Inngest endpoint |
// Define your event with IInngestEventData
public record OrderCreatedEvent : IInngestEventData
{
public static string EventName => "shop/order.created";
public required string OrderId { get; init; }
public required decimal Amount { get; init; }
}
// Send with full type safety - no magic strings!
public class OrderController : ControllerBase
{
private readonly IInngestClient _inngest;
public OrderController(IInngestClient inngest) => _inngest = inngest;
[HttpPost]
public async Task<IActionResult> CreateOrder(Order order)
{
// Strongly-typed - event name derived from OrderCreatedEvent.EventName
await _inngest.SendAsync(new OrderCreatedEvent
{
OrderId = order.Id,
Amount = order.Total
});
// With additional configuration
await _inngest.SendAsync(new OrderCreatedEvent
{
OrderId = order.Id,
Amount = order.Total
}, evt => evt.WithIdempotencyKey($"order-{order.Id}"));
return Ok();
}
}// Simple event with magic string
await _inngest.SendEventAsync("shop/order.created", new {
orderId = order.Id,
amount = order.Total
});
// Event with metadata
var evt = new InngestEvent("shop/order.created", new { orderId = order.Id })
.WithUser(new { id = order.CustomerId })
.WithIdempotencyKey($"order-{order.Id}");
await _inngest.SendEventAsync(evt);var result = await context.Step.Run("fetch-user", async () =>
{
var user = await userService.GetUserAsync(userId);
return user;
});
// Synchronous version
var value = await context.Step.Run("compute", () => ComputeValue());// Sleep for a duration
await context.Step.Sleep("wait", TimeSpan.FromHours(1));
// Sleep using Inngest time string format
await context.Step.Sleep("wait", "30m"); // 30 minutes
await context.Step.Sleep("wait", "2h30m"); // 2.5 hoursvar targetTime = DateTimeOffset.UtcNow.AddDays(1);
await context.Step.SleepUntil("wait-until-tomorrow", targetTime);var payment = await context.Step.WaitForEvent<PaymentData>(
"wait-payment",
new WaitForEventOptions
{
Event = "payment/completed",
Timeout = "24h",
Match = "async.data.orderId == event.data.orderId"
});
if (payment == null)
{
// Timeout occurred
await context.Step.Run("handle-timeout", () => CancelOrder());
}var result = await context.Step.Invoke<ProcessResult>(
"process-payment",
new InvokeOptions
{
FunctionId = "my-app-payment-processor",
Data = new { amount = 100, currency = "USD" },
Timeout = "5m"
});await context.Step.SendEvent("notify-team", new InngestEvent(
"notification/send",
new { message = "Order completed!", channel = "slack" }
));await context.Step.Run("validate", () =>
{
if (!IsValid(data))
{
// This error won't be retried
throw new NonRetriableException("Invalid data format");
}
return data;
});await context.Step.Run("call-api", async () =>
{
var response = await client.GetAsync(url);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Retry after the specified time
throw new RetryAfterException(TimeSpan.FromMinutes(5));
}
return await response.Content.ReadAsStringAsync();
});- .NET 8.0 SDK or later
- Node.js (for running the Inngest Dev Server via npx)
This approach tells the Dev Server where to find your app, enabling automatic function discovery.
Terminal 1 - Start your .NET app first:
dotnet run --project YourProjectTerminal 2 - Start the Dev Server pointing to your app:
npx inngest-cli@latest dev -u http://localhost:5000/api/inngestThe Dev Server will automatically discover and sync your functions.
If you prefer to start the Dev Server first:
Terminal 1 - Start the Dev Server:
npx inngest-cli@latest dev --no-discoveryTerminal 2 - Start your .NET app:
dotnet run --project YourProjectThe --no-discovery flag prevents auto-discovery. Your app will register its functions when it starts.
Note: Leave both terminals running during development.
Open your browser to http://localhost:8288 to:
- View all registered functions
- Send test events to trigger functions
- Monitor function executions in real-time
- Inspect step-by-step execution and retries
- Debug failures and view logs
For local development, you typically don't need to set any environment variables. The SDK auto-detects the Dev Server.
To explicitly configure the Dev Server URL:
export INNGEST_DEV=http://localhost:8288| Issue | Solution |
|---|---|
| Functions not appearing in Dev Server | Ensure your app is running and the /api/inngest endpoint is accessible |
| "Connection refused" errors | Check that the Dev Server is running on port 8288 |
| Events not triggering functions | Verify the event name matches your function's trigger exactly |
MIT