Lightweight, persistent background job queue for .NET. Supports priority scheduling, automatic retries, timeout enforcement, outbox pattern integration, and a built-in web dashboard.
A live demo is available at kododo.dev/runway/demo.
dotnet add package Kododo.RunWay
dotnet add package Kododo.RunWay.Runner
dotnet add package Kododo.RunWay.PostgreSQL
dotnet add package Kododo.RunWay.Dashboard # optionalpublic class SendEmailJob
{
public required string To { get; set; }
public required string Subject { get; set; }
}
public class SendEmailJobHandler : IJobHandler<SendEmailJob>
{
public async Task HandleAsync(SendEmailJob data, CancellationToken stoppingToken)
{
}
}builder.Services.AddRunWay(x =>
{
x.UsePostgreSQL(p => p.GetRequiredService<AppDbContext>().Database.GetDbConnection())
.AddRunner(opts => opts.AddHandlersFromAssembly(typeof(Program).Assembly))
.AddDashboard();
});public class OrderService(IScheduler scheduler)
{
public async Task PlaceOrderAsync(Order order, CancellationToken ct)
{
await scheduler
.Job(new SendEmailJob { To = order.Email, Subject = "Order confirmed" })
.ScheduleAsync(ct);
}
}app.UseRunWayDashboard(); // available at /schedulerawait scheduler
.Job(new SendEmailJob { To = "user@example.com", Subject = "Hello" })
.WithPriority(10) // higher = processed first (default: 0)
.WithRetryDelaysInSeconds(10, 60, 300) // retry after 10s, 60s, 5min
.WithTimeout(TimeSpan.FromMinutes(5)) // fail job if it exceeds this duration
.ScheduleAsync(ct);
// Schedule for a future time
await scheduler
.Job(new ReminderJob { Message = "Don't forget!" })
.ScheduleAsync(DateTimeOffset.UtcNow.AddHours(2), ct);Register recurring jobs using standard 5-field cron expressions. Shorthand like */5 * * * * is stored as-is, not expanded.
await app.SetRecurrenceAsync("hourly-report", "0 * * * *", new GenerateReportJob { ReportType = "hourly" });
await app.SetRecurrenceAsync("health-check", "*/5 * * * *", new HealthCheckJob());
await app.SetRecurrenceAsync("nightly-cleanup", "0 2 * * *", new CleanupJob(), opts =>
{
opts.WithTimeout(TimeSpan.FromMinutes(30))
.WithRetryDelaysInSeconds(60, 300);
});- Safe to call on every startup — only updates if the expression or data changed
- Each recurrence is identified by a unique string key
RunWay supports the outbox pattern — you can enlist job creation in your existing database transaction, guaranteeing atomicity between your business data and the scheduled job.
await using var transaction = await db.Database.BeginTransactionAsync();
db.Orders.Add(new Order { ... });
await db.SaveChangesAsync();
// AsTransactional(false) — reuse the ambient transaction instead of opening a new one
await scheduler
.Job(new SendEmailJob { To = "user@example.com", Subject = "Order confirmed" })
.AsTransactional(false)
.ScheduleAsync(ct);
// Both the order and the job are committed or rolled back together
await transaction.CommitAsync();For this to work, RunWay must share the same database connection as your DbContext:
x.UsePostgreSQL(p => p.GetRequiredService<AppDbContext>().Database.GetDbConnection()).AddRunner(opts =>
{
opts.ThreadsCount = 4; // parallel processing threads (default: processor count)
opts.Interval = TimeSpan.FromSeconds(5); // polling interval when queue is empty (default: 5s)
opts.HeartbeatInterval = TimeSpan.FromSeconds(30); // keep-alive signal interval (default: 30s)
opts.HeartbeatTimeout = TimeSpan.FromMinutes(2); // runner considered offline after this time (default: 2min)
opts.DeleteSucceededAfterTimeSpan = TimeSpan.FromHours(24); // auto-delete succeeded jobs (default: disabled)
})Mount the dashboard in your ASP.NET Core pipeline:
// Default path: /scheduler
app.UseRunWayDashboard();
// Custom path
app.UseRunWayDashboard("/jobs/dashboard");
// With authorization
app.UseRunWayDashboard()
.RequireAuthorization(policy => policy.RequireRole("Admin"));The dashboard shows job counts by status, a paginated job list, per-job audit timeline, recurring job schedules, and runner health.
- .NET 8, 9, or 10
- A storage provider: PostgreSQL 12 or later, or SQL Server 2016 or later (incl. Azure SQL Database)
MIT


