.NET API
IWorkflowRequestStore
Tracon.Abstractions.dllThe 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 IWorkflowRequestStoreRemarks
Section titled “Remarks”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.
Methods
Section titled “Methods”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)Parameters
Section titled “Parameters”records IReadOnlyList<WorkflowRequestRecord>
The rows. Every WorkflowRequestRecord.TenantId must be set and every WorkflowRequestRecord.Status must be WorkflowRequestStatus.Open.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”The completion task.
Remarks
Section titled “Remarks”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)Parameters
Section titled “Parameters”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.
Returns
Section titled “Returns”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.
Remarks
Section titled “Remarks”[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)Parameters
Section titled “Parameters”query WorkflowRequestQuery
The filter and the page.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”ValueTask<IReadOnlyList<WorkflowRequestRecord>>
The page of rows.
Remarks
Section titled “Remarks”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.
ListByRunAsync(Guid, CancellationToken)
Section titled “ ListByRunAsync(Guid, CancellationToken)”Lists every row of one run, open or closed, in request order.
ValueTask<IReadOnlyList<WorkflowRequestRecord>> ListByRunAsync(Guid runId, CancellationToken cancellationToken = default)Parameters
Section titled “Parameters”runId Guid
The run.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”max int
The upper number of rows returned.
cancellationToken CancellationToken
The cancellation token.
Returns
Section titled “Returns”ValueTask<IReadOnlyList<WorkflowRequestRecord>>
The rows (WorkflowRequestRecord.Status is WorkflowRequestStatus.Closed), oldest closure first; empty when no run is stranded.
Remarks
Section titled “Remarks”[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)Parameters
Section titled “Parameters”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.
Returns
Section titled “Returns”How many rows changed.
Remarks
Section titled “Remarks”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.