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.

BackWaveClient.cs
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.

EnqueueAsync.cs
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;
ChargeOrder.cs
// 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));
ParameterTypeDefaultMeaning
jobTJobrequiredThe payload instance, serialized via its registration.
dueTimeDateTimeOffsetrequiredWhen 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.
queuestring?nullnull uses the job type's registered Queue; a value overrides it for this call.
tagsJobTags?nullTags 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.
transactionDbTransaction?nullA caller-owned DbTransaction. When supplied, the job commits or rolls back atomically with the caller's own writes. Requires store.SupportsTransactionalEnqueue.
cancellationTokenCancellationTokendefaultStandard 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:

TransactionalEnqueue.cs
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 now

The 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:

ConditionExceptionparamNameMessage
transaction supplied but the store does not support it (checked before the store is touched)NotSupportedExceptionThis storage adapter does not support Transactional Enqueue (SupportsTransactionalEnqueue is false); enqueue without a transaction instead.
Serialized payload exceeds the store's boundArgumentExceptionjobPayload 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)InvalidOperationExceptionEnqueue 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.

EnqueueDependencyAsync.cs
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;
AfterCharge.cs
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.

ParameterTypeDefaultMeaning
parentIdGuidrequiredThe single gating parent. This method builds a one-element parent set.
enqueuedAtDateTimeOffset?null → the clock's nowWhen the dependent was created. A released dependent becomes due at release time if that is later than enqueuedAt.
modeDependencyModeOnSuccessOnSuccess runs only if the parent succeeded. Any other terminal outcome cancels the dependent. OnAnyTerminal runs once the parent is terminal, whatever the outcome.
queuestring?nullSame as EnqueueAsync: null uses the registered Queue.
tagsJobTags?nullSame 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.

UpsertRecurringAsync.cs
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;
NightlyRollup.cs
await client.UpsertRecurringAsync(
    "nightly-rollup",
    Cron.Daily(hour: 2),
    new RollupJob(),
    timeZone: "America/New_York");
ParameterTypeDefaultMeaning
scheduleIdstringrequiredThe stable id. Reusing it redefines the schedule in place.
cronCore.CronExpressionrequiredA parsed cron expression. Stored as its canonical six-field form.
templateTJobrequiredThe payload template; each minted instance carries a copy.
nowDateTimeOffset?null → the clock's nowThe instant the schedule begins watching from, stored as its cursor. Only ticks after now mint. Defining a schedule never back-fills past occurrences.
queuestring?nullnull uses the registered Queue.
timeZonestring?null → UTCIANA zone id (for example "America/New_York"). null evaluates the cron in UTC.
catchUpCatchUpPolicySkipSkip mints nothing for missed ticks; Coalesce mints exactly one make-up run for the whole missed set.
noOverlapboolfalseWhen 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.

RemoveRecurringAsync.cs
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.

BackWaveMonitor.cs
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.

HistoryPolicy.cs
public JobHistoryPolicy JobHistoryPolicy { get; }      // read straight from the store
public bool IsHistoryRecordingDisabled { get; }        // == (JobHistoryPolicy == Off)

The read methods#

MethodReturnsWhat 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.

BackWaveOperator.cs
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#

CancelJobAsync.cs
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#

RequeueAsync.cs
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#

PauseResumeQueue.cs
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#

SetConcurrencyLimitAsync.cs
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#

TriggerScheduleNowAsync.cs
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#

ListAuditRecordsAsync.cs
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.

MemberTypeNotes
JobIdGuidRequired.
WireNamestringRequired. The job type's stable string identity.
QueuestringRequired.
StateJobStateRequired.
AttemptintRequired. Execution tries so far; claiming to run starts an attempt.
DueTimeDateTimeOffsetRequired.
LeaseOwnerstring?The worker holding it while Leased, else null.
LeaseExpiryDateTimeOffset?While leased, else null.
CancelRequestedboolA cooperative cancel has been asked for.
TerminalAtDateTimeOffset?null while active.
TerminalCausestring?Short reason; null while active.
ScheduleIdstring?The minting schedule; null for a directly enqueued job.
SequencelongMonotonic paging cursor.
WorkflowIdGuid?null if not in a workflow.
TagsIReadOnlyList<JobTag>Defaults to empty.

JobPayloadView and PayloadEncoding#

GetJobPayloadAsync and GetJobOutputViewAsync return a JobPayloadView.

JobPayloadView.cs
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.

MemberTypeNotes
ScheduleIdstringRequired.
CronstringRequired. Canonical six-field form.
WireNamestringRequired.
QueuestringRequired.
CursorDateTimeOffsetRequired. Instant up to which ticks are resolved, inclusive.
TimeZoneIdstring?null means UTC.
CatchUpCatchUpPolicy
NoOverlapbool
NextDueDateTimeOffset?Next tick that will mint, or null if the cron has no future occurrence or the schedule cannot resolve.
HasLiveInstanceboolA minted instance is currently non-terminal, which is what No-Overlap watches.
SkippedTicksIReadOnlyList<DateTimeOffset>Recently skipped, newest last, bounded. Defaults to empty.
Errorstring?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.

MemberTypeDefaultMeaning
StateJobState?nullnull matches any state.
Queuestring?nullnull matches any queue.
WireNamestring?nullnull matches any type.
ScheduleIdstring?nullnull matches jobs from any source.
TagPredicatesIReadOnlyList<JobTagPredicate>[]AND-ed together and AND-composed with the scalar filters. OR is out of scope, so run two queries.
AfterSequencelong?nullPagination cursor; only jobs strictly beyond it in the requested direction are returned. null starts at page one.
SortDirectionJobSortDirectionOldestFirst
MaxResultsintint.MaxValueRequested 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.

FactoryMatches
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#

JobSortDirection.cs
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#

AggregateRecords.cs
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.

JobTransition.cs
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#

OperatorResults.cs
public enum CancelResult { CancelledImmediately, CancellationRequested, NotCancellable }
public enum RequeueResult { Requeued, NotRequeueable }
public enum TriggerScheduleResult { Triggered, ScheduleNotFound }
ValueMeaning
CancelledImmediatelyThe job was not yet running; it is now terminal Cancelled.
CancellationRequestedThe job was Leased; the cancel-requested flag is set and it cancels cooperatively on the next heartbeat.
NotCancellableThe job is absent or already terminal; nothing changed.
RequeuedA Dead-Lettered or Quarantined job returned to Scheduled with Attempt reset to 0.
NotRequeueableThe job is absent or not in a requeueable state.
TriggeredOne instance was minted immediately; the cursor did not move.
ScheduleNotFoundNo schedule with that id.

OperatorAction and OperatorAuditRecord#

OperatorAudit.cs
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.

ValueMeaning
OkCreated.
DuplicateA job with the same id already exists; nothing created, the existing job is left as-is.
PayloadTooLargeThe serialized payload exceeds the store's bound.
WireNameTooLongThe wire name exceeds the store's length bound.
UnknownParentA declared gating parent does not exist.
TooManyParentsThe declared parent set exceeds the store's maximum parent count.

DependencyMode#

DependencyMode.cs
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#

JobState.cs
public enum JobState
{
    Scheduled, AwaitingParent, Leased, Succeeded, Cancelled, DeadLettered, Quarantined
}
StateMeaning
ScheduledEnqueued, waiting for its due time; claimable once due.
AwaitingParentHeld until parents are terminal; becomes Scheduled once the last parent resolves.
LeasedClaimed by a worker, running under a heartbeat-renewed lease.
SucceededTerminal.
CancelledTerminal.
DeadLetteredTerminal.
QuarantinedTerminal.

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.

ValueRecords
OffNothing.
TransitionsTransition rows, with failure detail forced null.
TransitionsAndFailureDetailThe 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#

CatchUpPolicy.cs
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.

JobTag.cs
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.

JobTags.cs
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);
}
TaggingAtEnqueue.cs
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.

ObserverSubscription.cs
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" }.

ObserverReadTypes.cs
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 Cron builder and CronExpression behind UpsertRecurringAsync.
  • 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