Client, Monitor & Operator API
The three runtime public surfaces (the client that enqueues work and defines recurring schedules, the read-only monitor, and the audited operator actions), with the public types they take and return.
BackWave's runtime exposes three public surfaces. The client (BackWaveClient) puts work
into the system: it enqueues jobs for now or the future, enqueues jobs gated on a parent, and
defines Recurring Schedules. The monitor (BackWaveMonitor) reads the system without ever
changing it: single-job snapshots, filtered listings, queue depths, tag facets, schedule
status, and observer delivery health. The operator (BackWaveOperator) performs the
audited state-machine transitions (cancel, requeue, pause, resume,
set a concurrency limit, trigger a schedule), each stamped with the acting identity and
recorded in an append-only audit log.
This page is the API reference for all three, plus the public types they exchange. For the operational, task-oriented walkthrough of the monitor reads, see The Monitor API.
The client — BackWaveClient#
BackWaveClient lives in the BackWave namespace and is a sealed class. You construct it
with a job store, a job registry, and an optional clock.
public sealed class BackWaveClient
{
public BackWaveClient(IJobStore store, JobRegistry registry, TimeProvider? clock = null);
}The client owns the clock. When you omit clock it defaults to TimeProvider.System, and
every default instant the client needs (a job's due time, a dependent's enqueued-at, a
schedule's starting cursor) comes from that TimeProvider. You pass an explicit instant only
when you want to. Supplying a virtual clock is what makes enqueue-side timing deterministic
under Virtual Time. A future due time always defers; it is
never treated as "due now."
Each enqueue is serialized through the job type's registration, keyed by its Wire Name, and routed to the registered Queue unless a call overrides it.
Enqueue a job — EnqueueAsync#
EnqueueAsync is the primary entry point. It enqueues a job to become eligible at dueTime
and returns the new job's Guid id, which you can keep for later tracking or hand to
EnqueueDependencyAsync as a parent.
public async ValueTask<Guid> EnqueueAsync<TJob>(
TJob job,
DateTimeOffset dueTime,
string? queue = null,
JobTags? tags = null,
DbTransaction? transaction = null,
CancellationToken cancellationToken = default)
where TJob : notnull;// Enqueue for now: a due time of "now" runs as soon as a worker is free.
Guid id = await client.EnqueueAsync(new OrderCharged(orderId), DateTimeOffset.UtcNow);
// Defer: a future due time waits until then.
await client.EnqueueAsync(new SendReminder(orderId), DateTimeOffset.UtcNow.AddHours(24));| Parameter | Type | Default | Meaning |
|---|---|---|---|
job | TJob | required | The payload instance, serialized via its registration. |
dueTime | DateTimeOffset | required | When the job becomes eligible. Now runs as soon as a worker is free; a future instant defers. There is no zero-argument "enqueue now" overload, so pass the instant explicitly. |
queue | string? | null | null uses the job type's registered Queue; a value overrides it for this call. |
tags | JobTags? | null | Tags attached at enqueue. The job type's default tags are always unioned on top, additive only and never dropped at enqueue. Set semantics collapse identical tags. |
transaction | DbTransaction? | null | A caller-owned DbTransaction. When supplied, the job commits or rolls back atomically with the caller's own writes. Requires store.SupportsTransactionalEnqueue. |
cancellationToken | CancellationToken | default | Standard cancellation. |
Enqueuing "now" and scheduling for the future are the same code path: a plain enqueue is just a job whose due time is the current instant. The Scheduling page covers due time as the universal mechanism.
Transactional Enqueue. Passing a transaction writes the job inside your own database
transaction, so it is created if and only if your surrounding work commits, with no outbox
required. This is a store capability, not universal. If you supply a non-null transaction
while store.SupportsTransactionalEnqueue is false, the client throws before touching the
store:
await using var tx = await connection.BeginTransactionAsync();
await client.EnqueueAsync(new OrderCharged(orderId), DateTimeOffset.UtcNow, transaction: tx);
// ... your other writes on the same transaction ...
await tx.CommitAsync(); // the job becomes visible only nowThe W3C trace context, both the traceparent and the tracestate, is captured onto the job at
enqueue, so the eventual process span links back to the enqueue's send span (through an
ActivityLink, not a parent edge) even when the job runs hours later on another node. The send
span also records the call site of the enqueue, as code.function.name, code.file.path, and
code.line.number.
The client maps the store's result to an exception on any non-success outcome:
| Condition | Exception | paramName | Message |
|---|---|---|---|
transaction supplied but the store does not support it (checked before the store is touched) | NotSupportedException | — | This storage adapter does not support Transactional Enqueue (SupportsTransactionalEnqueue is false); enqueue without a transaction instead. |
| Serialized payload exceeds the store's bound | ArgumentException | job | Payload for wire name '{WireName}' is {n} bytes, which exceeds the MaxPayloadBytes bound. Store a reference (id, blob key) instead of the data itself. |
Any other non-Ok result (Duplicate, WireNameTooLong, TooManyParents) | InvalidOperationException | — | Enqueue failed: {result}. |
On success the client records the enqueue in diagnostics against the wire name and target queue.
Enqueue a dependent job — EnqueueDependencyAsync#
EnqueueDependencyAsync creates a job that waits for a single parent to reach a terminal state
before it is released. It returns the new dependent's Guid id.
public async ValueTask<Guid> EnqueueDependencyAsync<TJob>(
TJob job,
Guid parentId,
DateTimeOffset? enqueuedAt = null,
DependencyMode mode = DependencyMode.OnSuccess,
string? queue = null,
JobTags? tags = null,
CancellationToken cancellationToken = default)
where TJob : notnull;Guid chargeId = await client.EnqueueAsync(new OrderCharged(orderId), DateTimeOffset.UtcNow);
// Runs only if the charge succeeds; any other terminal outcome cancels it.
await client.EnqueueDependencyAsync(new SendReceipt(orderId), chargeId);The dependent sits in the AwaitingParent state until parentId becomes terminal, then
releases according to mode.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
parentId | Guid | required | The single gating parent. This method builds a one-element parent set. |
enqueuedAt | DateTimeOffset? | null → the clock's now | When the dependent was created. A released dependent becomes due at release time if that is later than enqueuedAt. |
mode | DependencyMode | OnSuccess | OnSuccess runs only if the parent succeeded. Any other terminal outcome cancels the dependent. OnAnyTerminal runs once the parent is terminal, whatever the outcome. |
queue | string? | null | Same as EnqueueAsync: null uses the registered Queue. |
tags | JobTags? | null | Same as EnqueueAsync: default tags are always unioned. |
There is no transaction parameter on this overload. On an unknown parent the store returns
UnknownParent and the client throws ArgumentException (paramName parentId) with message
Parent job {parentId} does not exist.; any other non-Ok result throws
InvalidOperationException with Enqueue failed: {result}.
This surface enqueues against exactly one parent. Fan-in on multiple parents and full dependency graphs are the Workflow builder, a Pro feature that is not part of the free client. Dependencies covers the single-parent latch in depth.
Define a Recurring Schedule — UpsertRecurringAsync#
UpsertRecurringAsync defines or redefines a Recurring Schedule: a cron template that mints
job instances on each tick. The schedule itself is not a job. Reusing a scheduleId redefines
that schedule in place.
public ValueTask UpsertRecurringAsync<TJob>(
string scheduleId,
Core.CronExpression cron,
TJob template,
DateTimeOffset? now = null,
string? queue = null,
string? timeZone = null,
CatchUpPolicy catchUp = CatchUpPolicy.Skip,
bool noOverlap = false,
CancellationToken cancellationToken = default)
where TJob : notnull;await client.UpsertRecurringAsync(
"nightly-rollup",
Cron.Daily(hour: 2),
new RollupJob(),
timeZone: "America/New_York");| Parameter | Type | Default | Meaning |
|---|---|---|---|
scheduleId | string | required | The stable id. Reusing it redefines the schedule in place. |
cron | Core.CronExpression | required | A parsed cron expression. Stored as its canonical six-field form. |
template | TJob | required | The payload template; each minted instance carries a copy. |
now | DateTimeOffset? | null → the clock's now | The instant the schedule begins watching from, stored as its cursor. Only ticks after now mint. Defining a schedule never back-fills past occurrences. |
queue | string? | null | null uses the registered Queue. |
timeZone | string? | null → UTC | IANA zone id (for example "America/New_York"). null evaluates the cron in UTC. |
catchUp | CatchUpPolicy | Skip | Skip mints nothing for missed ticks; Coalesce mints exactly one make-up run for the whole missed set. |
noOverlap | bool | false | When true, a tick is skipped while a previously minted instance is still non-terminal. The skip is always recorded. |
The cron is already a parsed CronExpression, so the only thing that can be wrong at this door
is the time zone: an unresolvable timeZone throws ArgumentException (paramName timeZone,
message from the resolver) before anything reaches storage. Build the cron value with the
Cron helpers or CronExpression.Parse. See the Cron API for the
builder, the accepted grammar, and how the canonical form is derived.
Remove a Recurring Schedule — RemoveRecurringAsync#
RemoveRecurringAsync stops future minting for a schedule.
public ValueTask RemoveRecurringAsync(
string scheduleId, CancellationToken cancellationToken = default);Already-minted instances are left untouched and run to completion. Removing a schedule that does not exist is a no-op and does not throw.
The monitor — BackWaveMonitor#
BackWaveMonitor lives in the BackWave.Monitor namespace and is a sealed class. Every
method is read-only and never mutates the store; every read returns a stable public shape and
never leaks storage internals.
public sealed class BackWaveMonitor
{
public BackWaveMonitor(IJobStore store, JobRegistry? registry = null);
}The registry is optional and is used only to answer GetKnownWireNames(). When you omit it,
that method returns an empty list and every other read still works.
History-policy properties#
Two properties tell you what the store is actually recording, so you can distinguish a genuinely empty timeline from one that is empty because history recording is off.
public JobHistoryPolicy JobHistoryPolicy { get; } // read straight from the store
public bool IsHistoryRecordingDisabled { get; } // == (JobHistoryPolicy == Off)The read methods#
| Method | Returns | What it answers |
|---|---|---|
GetKnownWireNames() | IReadOnlyList<string> | Every registered job type's Wire Name, alphabetical. Empty when no registry was supplied. |
GetJobAsync(jobId) | ValueTask<JobSnapshot?> | One job's current snapshot, or null when no such job exists. |
GetJobHistoryAsync(jobId) | ValueTask<IReadOnlyList<JobTransition>> | The append-only Transition Log for one job, oldest first. |
ListJobsAsync(query) | ValueTask<IReadOnlyList<JobSnapshot>> | Jobs matching a JobQuery, oldest first by default. |
GetJobPayloadAsync(jobId) | ValueTask<JobPayloadView?> | The job's payload rendered for display. Sensitive. |
GetJobOutputAsync(jobId) | ValueTask<ReadOnlyMemory<byte>?> | The raw Job Output bytes a handler emitted on success. |
GetJobOutputViewAsync(jobId) | ValueTask<JobPayloadView?> | The Job Output rendered for display. Sensitive. |
GetQueueDepthsAsync() | ValueTask<IReadOnlyList<QueueStateCount>> | Job counts grouped by (queue, state). |
GetTagFacetAsync(key, baseQuery) | ValueTask<IReadOnlyList<TagFacet>> | Distinct job counts grouped by one tag dimension. |
GetQueueSettingsAsync() | ValueTask<IReadOnlyList<QueueSettings>> | Each queue's Paused flag and configured ConcurrencyLimit. |
GetDependencyEdgesAsync(jobId) | ValueTask<DependencyEdges> | The dependency edges around one job. |
ListSchedulesAsync() | ValueTask<IReadOnlyList<ScheduleStatus>> | Every Recurring Schedule with cursor, next-due tick, and recent skips. |
GetObserverCursorAsync(observerId) | ValueTask<long> | One observer's durable delivery cursor. |
GetObserverLagAsync(observerId, subscription) | ValueTask<ObserverLag> | One observer's subscription-aware delivery lag. |
ListObserverDeadLettersAsync(observerId) | ValueTask<IReadOnlyList<ObserverDeadLetterRecord>> | One observer's dead-lettered deliveries, oldest first. |
Single-job reads. GetJobAsync returns null for an unknown id. GetJobHistoryAsync
returns the Transition Log oldest first (each entry a timestamp, resulting state, attempt
number, and optional failure detail) and returns empty both when the job is unknown and when
history recording is off. Use JobHistoryPolicy or IsHistoryRecordingDisabled to tell the
two cases apart.
Listing and paging. ListJobsAsync takes a JobQuery; passing null matches all jobs.
The returned page is capped by the store's maximum monitor page size. The store clamps
JobQuery.MaxResults down to that ceiling, so a request never returns every job at once. Page
further by passing the last row's JobSnapshot.Sequence as the next query's AfterSequence.
The cursor is direction-aware.
Payload and output are sensitive. GetJobPayloadAsync, GetJobOutputViewAsync, and the
raw GetJobOutputAsync are the only reads that surface payload or output bytes; a
JobSnapshot never carries them. The payload and view methods render the opaque bytes as
strict UTF-8, falling back to an uppercase hex dump for non-text bytes. BackWave itself does
not gate these. A payload or output may carry secrets or PII, so gate them behind the
ViewSensitiveData permission (or your own equivalent authorization) before you call.
GetJobOutputAsync returns the raw bytes for an in-process dependent to deserialize to the
producer's shape; it returns null when the job set no output. See
Reading a parent's output.
Aggregates. GetQueueDepthsAsync returns one QueueStateCount per (queue, state) pair
that currently has jobs, giving backlog depths, in-flight, and failure counts in a single read.
GetTagFacetAsync groups jobs by one tag dimension and counts distinct jobs per value, ordered
by count descending with value ascending as the tiebreak; a non-empty key facets a Keyed Tag
while the empty string "" facets plain Labels. GetQueueSettingsAsync reports each queue's
Paused flag and configured ConcurrencyLimit. In-use slots are not included, so derive them
from the Leased count in GetQueueDepthsAsync. Tags covers Labels
and Keyed Tags.
Dependency edges. GetDependencyEdgesAsync returns the edges around one job.
GatingParents is the set still gating it (a parent drops off once it completes, so this is
not the full original parent list), and Children are the jobs waiting on this one. Both sides
are empty for a job with no dependencies and for an unknown job.
Schedules. ListSchedulesAsync returns every Recurring Schedule with its cursor, next-due
tick, and recently skipped ticks. A schedule that cannot be resolved on this host (an
unparseable cron or an unknown time zone) is returned with ScheduleStatus.Error set rather
than dropped or thrown; NextDue is computed when the schedule is resolvable and is null
otherwise.
Observer delivery health. GetObserverCursorAsync returns one Transition Observer's
durable delivery cursor (the global timeline position through which all matching transitions
have been delivered or dead-lettered) and returns -1 when the observer has no cursor yet,
including when the observer id is unknown. GetObserverLagAsync is subscription-aware: it
counts only the transitions this observer would actually deliver, and throws
ArgumentNullException if observerId or subscription is null. ListObserverDeadLettersAsync
returns the observer's dead-lettered deliveries (those that exhausted the retry ceiling),
oldest first, as delivery metadata only, with no payload or failure detail. The
Observers API covers authoring observers.
Workflow reads are a Pro feature and are not part of the free monitor surface.
The operator — BackWaveOperator#
BackWaveOperator lives in the BackWave.Operations namespace and is a sealed class. It
performs the operator actions, each one a defined state-machine transition rather than a raw row edit.
Every action is stamped with the acting operator's identity and written to the append-only
operator audit log as exactly one record.
public sealed class BackWaveOperator
{
public BackWaveOperator(IJobStore store, TimeProvider? clock = null);
}clock defaults to TimeProvider.System. Every action method takes a string actor, an
optional DateTimeOffset? now = null (defaulting to the clock's current instant), and a
CancellationToken.
CancelJobAsync#
public ValueTask<CancelResult> CancelJobAsync(
Guid jobId, string actor, DateTimeOffset? now = null,
CancellationToken cancellationToken = default);A not-yet-started job cancels immediately. A running job is asked to stop cooperatively (its
handler's cancellation token fires via the heartbeat, and threads are never killed), and it
transitions to Cancelled when it next checks. The result is CancelledImmediately,
CancellationRequested, or NotCancellable (the job is absent or already terminal, so nothing
changed).
RequeueAsync#
public ValueTask<RequeueResult> RequeueAsync(
Guid jobId, string actor, DateTimeOffset? now = null,
CancellationToken cancellationToken = default);Requeues a Dead-Lettered or Quarantined job: it returns to Scheduled with its Attempt reset
to 0 and runs again. Any other state is rejected unchanged. The result is Requeued or
NotRequeueable.
PauseQueueAsync and ResumeQueueAsync#
public ValueTask PauseQueueAsync(
string queue, string actor, DateTimeOffset? now = null,
CancellationToken cancellationToken = default);
public ValueTask ResumeQueueAsync(
string queue, string actor, DateTimeOffset? now = null,
CancellationToken cancellationToken = default);Pausing a queue is cluster-wide: no worker claims from it until it is resumed. Jobs already running are unaffected. Resuming makes the queue's due jobs claimable again. Neither returns a value.
SetConcurrencyLimitAsync#
public ValueTask SetConcurrencyLimitAsync(
string queue, int? limit, string actor, DateTimeOffset? now = null,
CancellationToken cancellationToken = default);Sets or clears a queue's cluster-wide Concurrency Limit: at most limit of the queue's jobs
run at once across all workers, and limit == null removes the cap. It takes effect on the
next claim; running jobs are unaffected. Queues
covers the limit in full.
TriggerScheduleNowAsync#
public ValueTask<TriggerScheduleResult> TriggerScheduleNowAsync(
string scheduleId, string actor, DateTimeOffset? now = null,
CancellationToken cancellationToken = default);Mints one instance of a Recurring Schedule immediately, without moving the schedule's cursor or
disturbing its future ticks, a one-off, on-demand run. Here now is the instant the minted
instance becomes due. The result is Triggered or ScheduleNotFound.
ListAuditRecordsAsync#
public ValueTask<IReadOnlyList<OperatorAuditRecord>> ListAuditRecordsAsync(
string target, CancellationToken cancellationToken = default);Returns the operator audit trail for one target (a job id, a queue name, or a schedule id,
depending on the action type), oldest first. Every operator action contributes exactly one
record. Empty when none exist.
Workflow operator actions are a Pro feature and are not part of the free operator surface.
Public types#
The types below are the shapes these surfaces take and return.
JobSnapshot#
A sealed record of displayable facts about one job, with no payload bytes.
| Member | Type | Notes |
|---|---|---|
JobId | Guid | Required. |
WireName | string | Required. The job type's stable string identity. |
Queue | string | Required. |
State | JobState | Required. |
Attempt | int | Required. Execution tries so far; claiming to run starts an attempt. |
DueTime | DateTimeOffset | Required. |
LeaseOwner | string? | The worker holding it while Leased, else null. |
LeaseExpiry | DateTimeOffset? | While leased, else null. |
CancelRequested | bool | A cooperative cancel has been asked for. |
TerminalAt | DateTimeOffset? | null while active. |
TerminalCause | string? | Short reason; null while active. |
ScheduleId | string? | The minting schedule; null for a directly enqueued job. |
Sequence | long | Monotonic paging cursor. |
WorkflowId | Guid? | null if not in a workflow. |
Tags | IReadOnlyList<JobTag> | Defaults to empty. |
JobPayloadView and PayloadEncoding#
GetJobPayloadAsync and GetJobOutputViewAsync return a JobPayloadView.
public sealed record JobPayloadView
{
public required int ByteCount { get; init; } // raw length before rendering
public required PayloadEncoding Encoding { get; init; }
public required string Text { get; init; } // decoded UTF-8, or an uppercase hex dump
}
public enum PayloadEncoding { Utf8, Hex }Encoding is Utf8 when the bytes decoded cleanly as UTF-8, and Hex when they did not and
Text is an uppercase hex dump.
ScheduleStatus#
Returned by ListSchedulesAsync.
| Member | Type | Notes |
|---|---|---|
ScheduleId | string | Required. |
Cron | string | Required. Canonical six-field form. |
WireName | string | Required. |
Queue | string | Required. |
Cursor | DateTimeOffset | Required. Instant up to which ticks are resolved, inclusive. |
TimeZoneId | string? | null means UTC. |
CatchUp | CatchUpPolicy | |
NoOverlap | bool | |
NextDue | DateTimeOffset? | Next tick that will mint, or null if the cron has no future occurrence or the schedule cannot resolve. |
HasLiveInstance | bool | A minted instance is currently non-terminal, which is what No-Overlap watches. |
SkippedTicks | IReadOnlyList<DateTimeOffset> | Recently skipped, newest last, bounded. Defaults to empty. |
Error | string? | Non-null means the schedule is unresolvable on this host (unparseable cron or absent IANA zone); minting skips it. null when healthy. |
JobQuery#
The filter for ListJobsAsync. It is a sealed record; every null or empty field adds no
constraint and matches everything.
| Member | Type | Default | Meaning |
|---|---|---|---|
State | JobState? | null | null matches any state. |
Queue | string? | null | null matches any queue. |
WireName | string? | null | null matches any type. |
ScheduleId | string? | null | null matches jobs from any source. |
TagPredicates | IReadOnlyList<JobTagPredicate> | [] | AND-ed together and AND-composed with the scalar filters. OR is out of scope, so run two queries. |
AfterSequence | long? | null | Pagination cursor; only jobs strictly beyond it in the requested direction are returned. null starts at page one. |
SortDirection | JobSortDirection | OldestFirst | |
MaxResults | int | int.MaxValue | Requested page size. The store clamps it down to its maximum monitor page size, so the effective size is whatever the store allows. |
JobTagPredicate#
A sealed record built through static factories, used in JobQuery.TagPredicates. Predicates
on a query AND together.
| Factory | Matches |
|---|---|
HasLabel(string value) | The job carries the Label with this value. |
HasKeyValue(string key, string value) | The job carries this exact keyed tag. |
HasKey(string key) | The job carries any tag under this key, any value. |
It exposes string Key (empty string for a has-label predicate), string? Value (null to
match any value under Key), and bool Matches(JobTags tags).
JobSortDirection#
public enum JobSortDirection { OldestFirst, NewestFirst }OldestFirst is ascending by Sequence (the default; enqueue and claim order). NewestFirst
is descending, most recently enqueued first.
Aggregate result records#
public sealed record QueueStateCount(string Queue, JobState State, int Count);
public sealed record TagFacet(string Value, int Count);
public sealed record QueueSettings(string Queue, bool Paused, int? ConcurrencyLimit);
public sealed record DependencyEdges(
IReadOnlyList<Guid> GatingParents, IReadOnlyList<Guid> Children);For TagFacet, Value is the tag value (or the Label text for the empty-key facet) and
Count is distinct jobs; buckets are ordered by count descending, value ascending as the
tiebreak. For QueueSettings, ConcurrencyLimit is null when there is no cap, and in-use
slots are not included, so derive them from the Leased depth. For DependencyEdges,
GatingParents resolves away as each parent terminates, so it is not the full original set.
JobTransition#
An entry in a job's Transition Log, returned by GetJobHistoryAsync.
public sealed record JobTransition(
long Ordinal, DateTimeOffset Timestamp, JobState State, int Attempt, string? FailureDetail);Ordinal is a 0-based per-job sequence, oldest first, preserved even when older entries age
out beyond the cap. FailureDetail carries captured diagnostics (exception type, message,
stack), bounded for storage, and is null on every non-failing transition.
Operator result enums#
public enum CancelResult { CancelledImmediately, CancellationRequested, NotCancellable }
public enum RequeueResult { Requeued, NotRequeueable }
public enum TriggerScheduleResult { Triggered, ScheduleNotFound }| Value | Meaning |
|---|---|
CancelledImmediately | The job was not yet running; it is now terminal Cancelled. |
CancellationRequested | The job was Leased; the cancel-requested flag is set and it cancels cooperatively on the next heartbeat. |
NotCancellable | The job is absent or already terminal; nothing changed. |
Requeued | A Dead-Lettered or Quarantined job returned to Scheduled with Attempt reset to 0. |
NotRequeueable | The job is absent or not in a requeueable state. |
Triggered | One instance was minted immediately; the cursor did not move. |
ScheduleNotFound | No schedule with that id. |
OperatorAction and OperatorAuditRecord#
public enum OperatorAction
{
Cancel, Requeue, TriggerScheduleNow, PauseQueue, ResumeQueue, SetConcurrencyLimit
}
public sealed record OperatorAuditRecord(
string Actor, OperatorAction Action, string Target, DateTimeOffset RecordedAt);Target is a job id, queue name, or schedule id depending on the action.
EnqueueResult#
The store-level outcome the client maps to a return value or exception.
| Value | Meaning |
|---|---|
Ok | Created. |
Duplicate | A job with the same id already exists; nothing created, the existing job is left as-is. |
PayloadTooLarge | The serialized payload exceeds the store's bound. |
WireNameTooLong | The wire name exceeds the store's length bound. |
UnknownParent | A declared gating parent does not exist. |
TooManyParents | The declared parent set exceeds the store's maximum parent count. |
DependencyMode#
public enum DependencyMode { OnSuccess, OnAnyTerminal }OnSuccess releases only if every parent Succeeded. Any other terminal outcome cancels the
dependency. OnAnyTerminal releases once every parent is terminal, whatever the states.
JobState and JobStates#
public enum JobState
{
Scheduled, AwaitingParent, Leased, Succeeded, Cancelled, DeadLettered, Quarantined
}| State | Meaning |
|---|---|
Scheduled | Enqueued, waiting for its due time; claimable once due. |
AwaitingParent | Held until parents are terminal; becomes Scheduled once the last parent resolves. |
Leased | Claimed by a worker, running under a heartbeat-renewed lease. |
Succeeded | Terminal. |
Cancelled | Terminal. |
DeadLettered | Terminal. |
Quarantined | Terminal. |
JobStates.IsTerminal(this JobState) is true for Succeeded, Cancelled, DeadLettered,
and Quarantined. A terminal job never transitions on its own; only an explicit operator
action, such as requeue, can move it. The Job Lifecycle
covers the states and their transitions.
JobHistoryPolicy#
A ladder, where each rung adds to the one below. It gates what the store records, not the schema.
| Value | Records |
|---|---|
Off | Nothing. |
Transitions | Transition rows, with failure detail forced null. |
TransitionsAndFailureDetail | The full log. The default. |
A kill-switch environment variable, BACKWAVE_DISABLE_FAILURE_DETAIL, downgrades the top rung
to Transitions when set. Truthy values are "1", or "true", "yes", and "on"
(case-insensitive). See
Retention and purge for the operational side.
CatchUpPolicy#
public enum CatchUpPolicy { Skip, Coalesce }Skip (the default) mints nothing for missed ticks. Coalesce mints exactly one make-up run
for the whole missed set.
JobTag and JobTags#
A JobTag is either a Label (empty Key) or a Keyed Tag. Value is never empty. The kind is
what the empty key marks. A colon inside a value is ordinary data, not a parsed separator.
public sealed record JobTag
{
public string Key { get; } // empty for a Label
public string Value { get; } // never empty
public bool IsLabel => Key.Length == 0;
public static JobTag Label(string value); // throws if value is null/empty
public static JobTag Keyed(string key, string value); // throws if key or value is null/empty
}JobTags is an immutable set: re-adding an identical tag is a no-op, iteration is in first-seen
order, and equality is order-independent set equality.
public sealed class JobTags : IReadOnlyList<JobTag>, IEquatable<JobTags>
{
public static readonly JobTags Empty;
public JobTags WithLabel(string value);
public JobTags WithTag(string key, string value);
public JobTags With(JobTag tag);
public static JobTags From(IEnumerable<JobTag> tags); // collapses duplicates, keeps first-seen order
public bool Contains(JobTag tag);
}var tags = JobTags.Empty
.WithLabel("urgent")
.WithTag("tenant", "acme");
await client.EnqueueAsync(new OrderCharged(orderId), DateTimeOffset.UtcNow, tags: tags);Tags covers Labels, Keyed Tags, and set semantics in full.
Observer types#
GetObserverLagAsync takes an ObserverSubscription and the monitor returns ObserverLag;
ListObserverDeadLettersAsync returns ObserverDeadLetterRecord.
public sealed record ObserverSubscription(IReadOnlyList<JobState> States)
{
public static ObserverSubscription AllTransitions { get; } // every state
public string? WireName { get; init; } // null matches every type
public string? Queue { get; init; } // null matches every queue
public bool Matches(JobState state, string wireName, string queue);
}A transition into any state in States matches, further constrained by WireName and Queue
when set. Narrow AllTransitions with a with expression, for example
ObserverSubscription.AllTransitions with { WireName = "PaymentJob" }.
public sealed record ObserverLag(long Cursor, int Pending, DateTimeOffset? OldestPendingAt);
public sealed record ObserverDeadLetterRecord(
long Position, Guid JobId, long Ordinal, JobState State,
int Attempt, int DeliveryAttempts, DateTimeOffset DeadLetteredAt);In ObserverLag, Cursor is -1 when nothing has been delivered, Pending is 0 when the
observer is caught up, and OldestPendingAt is null when caught up. An
ObserverDeadLetterRecord is delivery metadata only, with no payload or failure detail. The
Observers API covers authoring and delivery.
Where to go next#
- The Monitor API: the task-oriented walkthrough of the monitor reads, including composing a health rollup and honoring the sensitive-data gate.
- Scheduling: due time as the universal mechanism, one-off future work, and Recurring Schedules.
- Cron API: the
Cronbuilder andCronExpressionbehindUpsertRecurringAsync. - Dependencies: the single-parent latch, reaction modes, and reading a parent's output.
- Tags: Labels, Keyed Tags, set semantics, and finding jobs by tag.
- Queues: named streams, concurrency limits, and pausing.
- The Job Lifecycle: every state and the transitions between them.
- Observers API: authoring Transition Observers whose delivery health the monitor reports.
Found a problem on this page? Report an issue