Lightweight, persistent background job queue for .NET — with priority scheduling, automatic retries, timeout support, 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 # optional// Job — the data your handler will receive
public class SendEmailJob
{
public required string To { get; set; }
public required string Subject { get; set; }
}
// Handler — the logic that processes the job
public class SendEmailJobHandler : IJobHandler<SendEmailJob>
{
public async Task HandleAsync(SendEmailJob data, CancellationToken stoppingToken)
{
// send email...
}
}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 on application startup using a standard cron expression. RunWay validates the expression at startup and stores it as-is — shorthand expressions like */5 * * * * are preserved.
// Simple recurring job
await app.SetRecurrenceAsync("hourly-report", "0 * * * *", new GenerateReportJob { ReportType = "hourly" });
// Every 5 minutes — stored as "*/5 * * * *", not expanded
await app.SetRecurrenceAsync("health-check", "*/5 * * * *", new HealthCheckJob());
// With job options
await app.SetRecurrenceAsync("nightly-cleanup", "0 2 * * *", new CleanupJob(), opts =>
{
opts.WithTimeout(TimeSpan.FromMinutes(30))
.WithRetryDelaysInSeconds(60, 300);
});- Standard 5-field cron syntax (minutes, hours, day, month, weekday)
- 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 provides an overview of job counts, a per-status job list with pagination, a detailed audit timeline for each job, recurring job schedules with cron expressions, and a runner overview with health status.
- .NET 8, 9, or 10
- PostgreSQL 12 or later (additional storage providers coming soon)
src/
├── RunWay.Core/ # Abstractions: IStore, IJobHandler, and job models
├── RunWay/ # Main package: DI registration and job scheduling
├── RunWay.Runner/ # Background worker
├── RunWay.Dashboard/ # Embedded SPA web dashboard
├── RunWay.EntityFramework/ # EF Core base (internal — not published as a package)
├── RunWay.PostgreSQL/ # PostgreSQL IStore implementation
└── RunWay.Demo.WebApp/ # Demo application
MIT