Workflow building blocks
Batches compose real patterns through the fluent builder: sequential
(RunJob<A>().ThenRunJob<B>()), parallel fan-out/fan-in (ThenInParallel(...)), approval
gates, and compensation (OnFailure(...)). This page covers the three building blocks most
people reach for first.
Approval gates
Section titled “Approval gates”Pause a batch until a human approves or rejects from the dashboard:
b.AddBatch("rollout", batch => batch .RunJob<DeployJob>() .ThenWaitForApproval( title: "Confirm rollout", roles: new[] { "ops" }, timeout: TimeSpan.FromMinutes(30), onTimeout: ApprovalTimeoutAction.Hold));The gate holds until an authenticated caller with a matching role approves it. Roles are
matched against ClaimTypes.Role by default — see Gotchas if
you use Azure AD / Auth0 / SAML.
A few more rules worth knowing:
- Set a timeout when the on-timeout action acts. If
onTimeoutisAutoApproveorHold, you must also give atimeout— otherwise the gate waits indefinitely and the action never fires. The wizard and the REST validators reject that combination.onTimeout: Failwith no timeout is valid: the gate just waits until a human decides. - A pending gate is a snapshot of its definition at creation time. Editing a batch definition does not change a gate that is already waiting — its roles, timeout, and on-timeout action are fixed when the run reaches the gate. A stuck, undecidable gate is resolved by deciding it through the approvals API with proper authentication, or by restarting the host.
Partitioned (data-parallel) jobs
Section titled “Partitioned (data-parallel) jobs”For “fetch a set of items, then process them on N workers”, implement
IPartitionedJob<TItem>. The runtime owns the producer/consumer plumbing (a bounded channel
plus N consumer tasks); you declare the source stream and the per-item work, with an optional
commit hook:
public sealed class ReconcileInvoicesJob : IPartitionedJob<int>{ // Stream the items to process. Yield lazily so the bounded channel applies backpressure. public async IAsyncEnumerable<int> SourceAsync(JobContext context, CancellationToken ct) { context.Progress.SetTotal(100); // drives a live x/100 progress counter for (var id = 1; id <= 100; id++) yield return id; }
// Runs on N concurrent workers — MUST be thread-safe. public Task ProcessAsync(int id, JobContext context, CancellationToken ct) => ReconcileAsync(id, ct);
// Optional commit hook: runs exactly once after every item, single-threaded. // Skipped on a fail-fast abort or cancellation; under ContinueOnError it commits the subset that succeeded. public Task FinalizeAsync(JobContext context, CancellationToken ct) => SaveResultsAsync(ct);}Register it with a worker count and a per-item error policy:
b.AddPartitionedJob<ReconcileInvoicesJob, int>() .Named("ReconcileInvoices") .WithParallelism(4) .WithItemErrorPolicy(ItemErrorPolicy.ContinueOnError);The worker count can be overridden per run by passing the trigger parameter
ukbatch.workers (an invalid value falls back to the configured parallelism with a warning,
and the effective count is capped at 128).
Attribute discovery
Section titled “Attribute discovery”Instead of registering each job explicitly, decorate it with [Job] and scan assemblies:
[Job(Name = "DailyReport", Schedule = "0 0 9 * * *", MaxRetries = 3, TimeoutSeconds = 600)]public sealed class DailyReportJob : IJob { /* ... */ }
builder.AddUKBatchAspNetCore(b => b.ScanAssemblies(typeof(Program).Assembly));[Job] carries optional Name, Schedule (cron), MaxRetries, TimeoutSeconds, and Tags.