Skip to content
Tracon

IWorkflowRequestStore

Namespace Tracon · Assembly Tracon.Abstractions.dll

The inbox of a tenant’s open workflow requests: one row per request a run is waiting on, with the time the request expires and whether somebody answered it.

public interface IWorkflowRequestStore

This is a projection, not the request. The request itself (its prompt, its form) stays in the run’s RunEventType.WorkflowRequest event; a row names that event by its sequence number and carries only what a listing and the expiry need to filter on. A row is written once, when the run closes as RunStatus.AwaitingInput, and a row that cannot be written does not stop the run: the request is still listed by GET /api/workflows/runs/{runId}/requests and can still be answered, but it is missing from the tenant-wide list and never expires.

Tenant behavior. IWorkflowRequestStore.ListAsync, IWorkflowRequestStore.ListByRunAsync and IWorkflowRequestStore.MarkAnsweredAsync see only the caller’s tenant (ITenantContext); a write uses the tenant the record carries. IWorkflowRequestStore.ExpireAsync is maintenance work and scans every tenant, the same reason as IPendingApprovalStore.ExpireAsync.

DI lifetime. Registered as a singleton with TryAdd; a consumer’s own registration wins, and each SQL provider replaces the in-memory default. An implementation must be safe under concurrent calls: an answer and the expiry of the same request arrive together by design.

Delivery. No delivery guarantee applies: every call completes or throws before it returns, and nothing is sent later. The engine’s write of a run’s rows and the endpoint’s IWorkflowRequestStore.MarkAnsweredAsync call are best-effort: a failure is logged and the run, or the answer, goes on. IWorkflowRequestStore.ExpireAsync hands each closed row to exactly one caller.

A SQL store ties a row to its run with a cascading foreign key, so a data subject erasure that removes the run removes its rows. A row of a run that was closed is deleted by the RetentionTargets.WorkflowRequests retention target.

AddAsync(IReadOnlyList<WorkflowRequestRecord>, CancellationToken)

Section titled “ AddAsync(IReadOnlyList<WorkflowRequestRecord>, CancellationToken)”

Writes the rows of the requests a run is waiting on.

ValueTask AddAsync(IReadOnlyList<WorkflowRequestRecord> records, CancellationToken cancellationToken = default)

records IReadOnlyList<WorkflowRequestRecord>

The rows. Every WorkflowRequestRecord.TenantId must be set and every WorkflowRequestRecord.Status must be WorkflowRequestStatus.Open.

cancellationToken CancellationToken

The cancellation token.

ValueTask

The completion task.

A row whose run and request id are already stored is left as it is, so writing the same run twice is harmless. The run must exist: a SQL store refuses a row for a run it does not hold.

ExpireAsync(DateTimeOffset, int, CancellationToken)

Section titled “ ExpireAsync(DateTimeOffset, int, CancellationToken)”

Closes the open rows whose WorkflowRequestRecord.ExpiresAt has passed and returns them.

ValueTask<IReadOnlyList<WorkflowRequestRecord>> ExpireAsync(DateTimeOffset now, int max, CancellationToken cancellationToken = default)

now DateTimeOffset

Rows that expire at or before this time are closed (UTC).

max int

The upper number of rows closed in this round.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<WorkflowRequestRecord>>

The rows that were closed (WorkflowRequestRecord.Status is WorkflowRequestStatus.Closed); WorkflowRequestRecord.AnsweredAt tells whether the request was answered. The order of the rows is not defined.

[TenantAgnostic]: this is maintenance work and scans the expired rows of every tenant; filtering by the ambient tenant would leave the requests of the other tenants open forever. Two callers that close the same row are decided by the store: one receives the row.

ListAsync(WorkflowRequestQuery, CancellationToken)

Section titled “ ListAsync(WorkflowRequestQuery, CancellationToken)”

Lists the caller’s open requests, one per logical request, oldest first.

ValueTask<IReadOnlyList<WorkflowRequestRecord>> ListAsync(WorkflowRequestQuery query, CancellationToken cancellationToken = default)

query WorkflowRequestQuery

The filter and the page.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<WorkflowRequestRecord>>

The page of rows.

A request is logical: the same request id in the same execution session is one request even when several runs of the session wait on it (a run that is answered stays RunStatus.AwaitingInput, and the continuation republishes the requests that are still unanswered). The listing returns the newest open row of each, so the row names the run to answer.

The state filter and the page are applied after that choice, in the store, so a page is never shorter than WorkflowRequestQuery.Take unless the listing ends.

The order is WorkflowRequestRecord.RequestedAt, then run id, then request id; it is stable, so WorkflowRequestQuery.Skip pages without gaps or repeats while nothing changes.

Lists every row of one run, open or closed, in request order.

ValueTask<IReadOnlyList<WorkflowRequestRecord>> ListByRunAsync(Guid runId, CancellationToken cancellationToken = default)

runId Guid

The run.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<WorkflowRequestRecord>>

The rows of the caller’s tenant; empty when the run has none.

ListStrandedAsync(int, CancellationToken)

Section titled “ ListStrandedAsync(int, CancellationToken)”

Lists the closed rows of runs that still wait and have no open row left: the trace of a process that stopped after IWorkflowRequestStore.ExpireAsync closed the last row of a run and before the run itself was closed.

ValueTask<IReadOnlyList<WorkflowRequestRecord>> ListStrandedAsync(int max, CancellationToken cancellationToken = default)

max int

The upper number of rows returned.

cancellationToken CancellationToken

The cancellation token.

ValueTask<IReadOnlyList<WorkflowRequestRecord>>

The rows (WorkflowRequestRecord.Status is WorkflowRequestStatus.Closed), oldest closure first; empty when no run is stranded.

[TenantAgnostic]: the same maintenance work as IWorkflowRequestStore.ExpireAsync, across every tenant. A closed row is never returned by IWorkflowRequestStore.ExpireAsync again, so without this listing a run closed by nobody would stay RunStatus.AwaitingInput and retention would keep its request event, checkpoints and input row for good.

A store whose rows do not outlive the process (the in-memory store) cannot be left in that state and returns an empty list.

MarkAnsweredAsync(string, string, Guid, DateTimeOffset, CancellationToken)

Section titled “ MarkAnsweredAsync(string, string, Guid, DateTimeOffset, CancellationToken)”

Records that a request was answered: every open row of the logical request (the same request id in the same session, whichever run holds it) becomes answered.

ValueTask<int> MarkAnsweredAsync(string sessionId, string requestId, Guid responseRunId, DateTimeOffset answeredAt, CancellationToken cancellationToken = default)

sessionId string

The execution session the request belongs to.

requestId string

The request id.

responseRunId Guid

The run the answer started.

answeredAt DateTimeOffset

When the answer was accepted (UTC).

cancellationToken CancellationToken

The cancellation token.

ValueTask<int>

How many rows changed.

Marking a request that is already answered replaces the run it names and keeps the first WorkflowRequestRecord.AnsweredAt: the answer that replaces a stopped one (WorkflowResponseStatus.Abandoned) names its own run. A row that is closed is not touched.