Skip to content

Triggering and running

Use IJobClient to run jobs from endpoints, other jobs, or background services.

The methods come in two flavors:

  • Trigger and own (Run*, Stream*): start work and consume its outcome. Cancelling your CancellationToken cancels the run or batch you started.
  • Observe (Wait*, Observe*): consume a run or batch that already exists. Cancelling your token only stops your observer; the run keeps going.

TriggerAsync enqueues a run and returns immediately. The returned JobRun carries the run ID and initial status.

// Fire and forget
var run = await client.TriggerAsync("Cleanup");
// With arguments
await client.TriggerAsync("Add", new { a = 1, b = 2 });
// Schedule for later
await client.TriggerAsync("Report", null, new RunOptions
{
NotBefore = DateTimeOffset.UtcNow.AddHours(1)
});
// With a higher priority (claimed before lower-priority pending runs)
await client.TriggerAsync("Urgent", null, new RunOptions { Priority = 10 });

Concurrency and rate limits are enforced when runs are claimed, not at trigger time. TriggerAsync always returns a Pending run, even if the queue is full.

RunAsync<T> triggers a job and waits for its result.

var sum = await client.RunAsync<int>("Add", new { a = 1, b = 2 });

For jobs without a result, use the non-generic overload. It throws JobRunException if the run terminates non-successfully.

await client.RunAsync("Cleanup");

When a job triggers another, the child carries the parent’s ParentRunId so the dashboard can render the trace tree. Cancelling the parent cascades to its descendants.

app.AddJob("AddRandom", async (IJobClient client, CancellationToken ct) =>
{
var a = Random.Shared.Next(1, 101);
var b = Random.Shared.Next(1, 101);
var sum = await client.RunAsync<int>("Add", new { a, b }, cancellationToken: ct);
return new { a, b, sum };
});

If a job returns IAsyncEnumerable<T>, consume its output as a stream:

await foreach (var item in client.StreamAsync<Order>("StreamOrders"))
{
// process item
}

See Streaming for more information.

A batch fans out a single job over many inputs, or runs a mix of jobs together. Each input becomes a child run.

// One job, many inputs
var results = await client.RunBatchAsync<Result>("ProcessOrder", new[]
{
new { orderId = 101 },
new { orderId = 102 },
new { orderId = 103 }
});
// Different jobs in one batch
var mixed = await client.RunBatchAsync(new[]
{
new BatchItem("ProcessOrder", new { orderId = 101 }),
new BatchItem("EmailReceipt", new { orderId = 101 })
});

RunBatchAsync<T> waits for every child to terminate, then returns the results in commit order. If any child failed or was canceled, it throws AggregateException at the end.

To consume results as each child finishes instead of waiting for the whole batch, use StreamBatchAsync<T>:

await foreach (var item in client.StreamBatchAsync<Result>("ProcessOrder", inputs))
{
// process item
}

StreamBatchAsync<T> is fail-fast: it throws on the first non-success child, even though the rest of the batch keeps running.

The Wait* and Observe* methods observe a run or batch without owning it. Useful when you didn’t trigger the work but still need to react to it: dashboards, monitoring, code waiting on a run someone else started.

// Wait for a terminal status and return the final snapshot
var run = await client.WaitAsync(runId);
// Wait for a terminal status and deserialize the result
var sum = await client.WaitAsync<int>(runId);
// Stream output items from an existing run
await foreach (var item in client.WaitStreamAsync<Order>(runId))
{
// process item
}
// Yield each child of a batch as it terminates
await foreach (var child in client.WaitEachAsync(batchId))
{
// process child
}
// Stream raw events for a run (Output, Progress, Log, status events, etc.)
await foreach (var @event in client.ObserveRunEventsAsync(runId))
{
// process event
}

Pass sinceEventId to resume an event stream where you left off.

OptionDescription
NotBeforeDon’t run before this time
NotAfterCancel the run if it hasn’t started by this time
PriorityHigher-priority runs are claimed first
DeduplicationIdPrevents duplicate runs with the same ID

A deduplication ID makes sure only one run with a given ID is active at a time. While that run is non-terminal, any trigger using the same ID throws RunConflictException. The ID frees up once the run reaches a terminal status.

await client.TriggerAsync("Import", args, new RunOptions
{
DeduplicationId = $"import-{date:yyyy-MM-dd}"
});

Set NotAfter to give a run a deadline. If it hasn’t started by that time, Surefire cancels it automatically. Useful for time-sensitive work where a late execution is worse than no execution.

await client.TriggerAsync("TimelyReport", args, new RunOptions
{
NotAfter = DateTimeOffset.UtcNow.AddMinutes(30)
});

NotAfter only cancels runs that haven’t started yet. To stop a run that’s already executing, cancel it explicitly with CancelAsync.

// Cancel a run (and any descendants it triggered)
await client.CancelAsync(runId);
// Cancel a whole batch
await client.CancelBatchAsync(batchId);
// Re-execute a finished run with the same arguments and inputs
var rerun = await client.RerunAsync(runId);

A rerun creates a new run with the same arguments and input events. See Job lifecycle for how reruns differ from retries.

// Single run
var run = await client.GetRunAsync(runId);
// Filtered query, paged automatically
await foreach (var item in client.GetRunsAsync(new RunFilter
{
JobName = "DataImport",
Status = JobStatus.Running
}))
{
// process item
}