Inngest.NET
1.4.5
dotnet add package Inngest.NET --version 1.4.5
NuGet\Install-Package Inngest.NET -Version 1.4.5
<PackageReference Include="Inngest.NET" Version="1.4.5" />
<PackageVersion Include="Inngest.NET" Version="1.4.5" />
<PackageReference Include="Inngest.NET" />
paket add Inngest.NET --version 1.4.5
#r "nuget: Inngest.NET, 1.4.5"
#:package Inngest.NET@1.4.5
#addin nuget:?package=Inngest.NET&version=1.4.5
#tool nuget:?package=Inngest.NET&version=1.4.5
Inngest.NET
A .NET SDK for Inngest, a platform for building reliable, scalable event-driven workflows.
Features
- 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
Installation
dotnet add package Inngest.NET
Quick Start
1. Configure Inngest in Program.cs
using 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();
2. Create a Function
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 };
}
}
3. Strongly-Typed Events (No Magic Strings!)
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"
});
Function Attributes
InngestFunction
Marks a class as an Inngest function:
[InngestFunction("my-function-id", Name = "Human Readable Name")]
EventTrigger
Triggers the function when a specific event is received:
[EventTrigger("user/signed.up")]
[EventTrigger("user/invited", Expression = "event.data.role == 'admin'")] // With filter
CronTrigger
Triggers the function on a schedule:
[CronTrigger("0 0 * * *")] // Every day at midnight
[CronTrigger("*/30 * * * *")] // Every 30 minutes
Retry
Configures retry behavior:
[Retry(Attempts = 5)]
Concurrency
Limits concurrent executions:
[Concurrency(5)] // Max 5 concurrent executions
[Concurrency(1, Key = "event.data.userId")] // Per-user concurrency
RateLimit
Limits execution rate:
[RateLimit(100, Period = "1h")] // 100 per hour
[RateLimit(10, Period = "1m", Key = "event.data.customerId")] // Per-customer
Configuration
Using Options Pattern
// 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);
Using Action Configuration
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>();
Environment Variables
| 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 |
Sending Events
Strongly-Typed Events (Recommended)
// 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();
}
}
Untyped Events (Legacy)
// 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);
Step Primitives
step.Run - Execute code with automatic retry
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());
step.Sleep - Pause execution
// 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 hours
step.SleepUntil - Sleep until a specific time
var targetTime = DateTimeOffset.UtcNow.AddDays(1);
await context.Step.SleepUntil("wait-until-tomorrow", targetTime);
step.WaitForEvent - Wait for another event
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());
}
step.Invoke - Call another Inngest function
var result = await context.Step.Invoke<ProcessResult>(
"process-payment",
new InvokeOptions
{
FunctionId = "my-app-payment-processor",
Data = new { amount = 100, currency = "USD" },
Timeout = "5m"
});
step.SendEvent - Emit events from within a function
await context.Step.SendEvent("notify-team", new InngestEvent(
"notification/send",
new { message = "Order completed!", channel = "slack" }
));
Error Handling
Non-Retriable Errors
await context.Step.Run("validate", () =>
{
if (!IsValid(data))
{
// This error won't be retried
throw new NonRetriableException("Invalid data format");
}
return data;
});
Retry-After Errors
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();
});
Running Locally
Prerequisites
- .NET 8.0 SDK or later
- Node.js (for running the Inngest Dev Server via npx)
Option A: Auto-Discovery with -u Flag (Recommended)
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 YourProject
Terminal 2 - Start the Dev Server pointing to your app:
npx inngest-cli@latest dev -u http://localhost:5000/api/inngest
The Dev Server will automatically discover and sync your functions.
Option B: Manual Registration
If you prefer to start the Dev Server first:
Terminal 1 - Start the Dev Server:
npx inngest-cli@latest dev --no-discovery
Terminal 2 - Start your .NET app:
dotnet run --project YourProject
The --no-discovery flag prevents auto-discovery. Your app will register its functions when it starts.
Note: Leave both terminals running during development.
Access the Dev Server UI
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
Environment Variables for Local Development
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
Troubleshooting
| 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 |
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.5)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.5)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.5)
- Microsoft.Extensions.Http (>= 9.0.5)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.5)
- Microsoft.Extensions.Options (>= 9.0.5)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.5)
- System.Text.Json (>= 9.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.4.5 | 302 | 5/23/2026 |
| 1.4.4 | 10,070 | 2/10/2026 |
| 1.4.3 | 1,219 | 1/28/2026 |
| 1.4.2 | 853 | 1/16/2026 |
| 1.4.1 | 710 | 1/6/2026 |
| 1.4.0 | 264 | 1/5/2026 |
| 1.3.7 | 1,410 | 12/15/2025 |
| 1.3.6 | 316 | 12/15/2025 |
| 1.3.5 | 307 | 12/15/2025 |
| 1.3.4 | 345 | 12/13/2025 |
| 1.3.3 | 209 | 12/13/2025 |
| 1.3.2 | 195 | 12/13/2025 |
| 1.3.1 | 200 | 12/13/2025 |
| 1.3.0 | 199 | 12/13/2025 |
| 1.2.5 | 171 | 12/13/2025 |
| 1.2.4 | 154 | 12/13/2025 |
| 1.2.3 | 149 | 12/13/2025 |
| 1.2.2 | 153 | 12/13/2025 |
| 1.2.1 | 156 | 12/13/2025 |
| 1.2.0 | 153 | 12/12/2025 |