Skip to content

Commit 25184ff

Browse files
authored
feat(core): evented workflow engine for background tasks + suspend/resume (#16260)
## Summary - Background tasks now execute as evented workflows (each task is a registered `__background-task` workflow run). The legacy in-manager dispatch loop is preserved behind `engine: 'legacy'`. - Adds suspend/resume to background tasks. Tools can call `ctx.agent.suspend(data)` from inside `execute` to park their bg task; resume with `mastra.backgroundTaskManager.resume(taskId, resumeData)` or `agent.resumeStreamUntilIdle(resumeData, { runId, toolCallId })`. - New `agent.resumeStreamUntilIdle()` (and corresponding HTTP routes / `@mastra/client-js` methods) resumes a suspended agent stream and keeps the SSE stream open through bg-task completion or suspension and the agent's follow-up turn. - Storage adapters track two new fields: `suspendPayload` (JSON) and `suspendedAt` (timestamp). SQL adapters auto-migrate via `alterTable.ifNotExists`. ## Why The previous in-manager dispatch loop owned executor invocation, retries, and lifecycle directly. That made suspend/resume difficult — there was no durable cursor to park the run on. Routing each task through an evented workflow gives us snapshotting + resume for free, and lets `agent.streamUntilIdle()` close cleanly when a bg task suspends rather than waiting indefinitely for completed/failed/cancelled. Pubsub topics, lifecycle event shapes, concurrency gating, and the `BackgroundTaskManager.stream()` contract are unchanged — the workflow engine is plumbed underneath the existing seams. ## Migration notes - **Default engine flip.** Background-tasks default to `engine: 'workflow'`. Set `backgroundTasks: { engine: 'legacy' }` to keep the previous in-manager dispatch loop. `manager.resume(...)` only works with the workflow engine. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## ELI5 Background tasks can now pause themselves, save their state, and be resumed later (manually or via a streaming agent). Tasks run as durable evented workflows for reliability and resumeability while the old dispatch loop remains available behind a config flag. --- ## Overview Runs background tasks as evented workflow runs (registered as the __background-task workflow) and adds durable suspend/resume semantics. The workflow engine is integrated under existing seams so pubsub topics, lifecycle event shapes, concurrency gating, and BackgroundTaskManager.stream() remain compatible. The legacy in-manager dispatch loop is preserved behind backgroundTasks.engine = 'legacy'. ## Key Changes - Workflow-backed background tasks - New BACKGROUND_TASK_WORKFLOW_ID ('__background-task') and buildBackgroundTaskWorkflow(manager) implementing retries, timeouts, aborts, progress throttling, persistence, and durable suspend/resume. - Internal workflow resolution updated so Mastra can register/use internal workflows. - Suspend / resume - Tools may call ctx.agent.suspend(data) inside execute to persist suspendPayload, set suspendedAt, snapshot execution, and release concurrency slots. - Resume via mastra.backgroundTaskManager.resume(taskId, resumeData) (workflow engine only) or agent.resumeStreamUntilIdle(resumeData, { runId, toolCallId }) for in-band streaming resumes. - BackgroundTaskHandle gains checkIfSuspended(...) and resume(...). - Streaming API - New Agent API/method: resumeStreamUntilIdle(...) plus server route POST /api/agents/:agentId/resume-stream-until-idle and client-js helper. Keeps SSE open through resume completion/suspension and the agent’s follow-up turn. - streamUntilIdle/resume flows treat background-task-suspended as terminal-for-wrapper (enqueued so callers can trigger continuations). - Lifecycle & events - New stream chunk types: background-task-suspended and background-task-resumed with payload shapes. - BackgroundTaskManager: new async resume(taskId, resumeData?), runLocalSuspendHooks, runLocalCompletionHooks; publishLifecycleEvent now accepts suspended/resumed types. taskContexts and activeAbortControllers exposed for workflow steps. - Continuation dedup strengthened: processed terminal events tracked by `${taskId}:${chunkType}` to avoid suspend→resume→complete collisions. - stream/continuation logic updated to track suspended vs running tool-call IDs. - Storage, schema & adapters - Added suspend_payload (suspendPayload) and suspendedAt (timestamp) to background-tasks schema; many adapters auto-migrate via alterTable.ifNotExists. - Listing/deletion now support toolCallId filtering and suspendedAt-aware date filtering/ordering across adapters (Postgres, MSSQL, LibSQL, ClickHouse, Cloudflare D1/DO/KV, DynamoDB, Convex, Lance, MongoDB, Upstash, etc.). - In-memory and other stores updated to parse/serialize suspendPayload and suspendedAt. - Types, client libs & UI - BackgroundTaskStatus includes 'suspended'; BackgroundTask gains suspendPayload?: unknown and suspendedAt?: Date. - MastraClient.listBackgroundTasks accepts optional toolName and toolCallId filters. - client-js: route/typing for resume-stream-until-idle and resumeStreamUntilIdle client method (maxIdleMs option). - React/toUIMessage and MastraUIMessageMetadata updated to surface suspendedAt and restore suspend metadata after resume. - Playground UI components updated to display suspend payload and suspendedAt; some component prop shapes simplified to pass a backgroundTask object. - Examples & tests - Example tool cryptoResearchTool converted to a suspend/resume workflow (approval flow); some agents set autoResumeSuspendedTools. - Extensive unit/e2e tests added/updated to start/stop the event engine and exercise suspend/resume, resume-stream-until-idle, and deterministic manager behavior. - New workflow registration tests verify internal workflow registration behavior. ## Notable public API / signature changes - New exports/types: BACKGROUND_TASK_WORKFLOW_ID; BackgroundTaskSuspended/Resumed chunk payloads; CheckIfSuspendedPayload. - BackgroundTaskManager: new async resume(taskId, resumeData?) and public runLocalSuspendHooks/runLocalCompletionHooks; taskContexts and activeAbortControllers are no longer private. - Agent: added resumeStreamUntilIdle overloads (streaming resume path). - BackgroundTaskHandle: checkIfSuspended(...) and resume(...) methods. - client-js: new resumeStreamUntilIdle method and route types; MastraClient.listBackgroundTasks query supports toolName/toolCallId. - React/playground components: BackgroundTaskMetadataDialogTrigger prop shape changed to accept a backgroundTask object. ## Docs - docs: added “Suspending and resuming” section, examples, and mention of workflow engine requirement. - ChunkType docs updated with background-task-suspended/resumed payloads. - client-js docs updated with resumeStreamUntilIdle usage. ## Migration & compatibility - Default backgroundTasks.engine is now 'workflow'. Set backgroundTasks: { engine: 'legacy' } to retain previous dispatch loop. - manager.resume(...) only works with the workflow engine. - Pubsub topics, lifecycle chunk shapes, concurrency gating, and BackgroundTaskManager.stream() contract remain backward-compatible. - Storage migrations run automatically on init for adapters that support alterTable.ifNotExists(). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent cd208b6 commit 25184ff

65 files changed

Lines changed: 3259 additions & 605 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/short-cougars-poke.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@mastra/cloudflare-d1': patch
3+
'@mastra/clickhouse': patch
4+
'@mastra/cloudflare': patch
5+
'@mastra/dynamodb': patch
6+
'@mastra/mongodb': patch
7+
'@mastra/upstash': patch
8+
'@mastra/convex': patch
9+
'@mastra/libsql': patch
10+
'@mastra/lance': patch
11+
'@mastra/mssql': patch
12+
'@mastra/pg': patch
13+
---
14+
15+
Track `suspendedAt` and `suspendPayload` on background tasks. SQL adapters auto-migrate the new columns via `alterTable`.

.changeset/slow-monkeys-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/react': patch
3+
---
4+
5+
Handle `background-task-suspended` chunks in `toUIMessage` so suspend metadata is restored after resume.

.changeset/some-turtles-write.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/client-js': minor
3+
---
4+
5+
Add `agent.resumeStreamUntilIdle()` to resume a suspended agent stream and keep the SSE connection open through the follow-up turn.

.changeset/sour-owls-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/server': minor
3+
---
4+
5+
Add `POST /api/agents/:agentId/resume-stream-until-idle` SSE route, mirroring `agent.resumeStreamUntilIdle()`.

.changeset/wet-shirts-own.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Background tasks now run as evented workflows. Each task is dispatched as a workflow run that owns executor invocation, retries, and suspend/resume; pubsub topics, lifecycle event shapes, concurrency gating, and the `BackgroundTaskManager.stream()` contract are unchanged.
6+
7+
Add suspend/resume to background tasks. Tools can call `ctx.agent.suspend(data)` from `execute` to pause a task and release the concurrency slot; resume with `mastra.backgroundTaskManager.resume(taskId, resumeData)` or `agent.resumeStreamUntilIdle(resumeData, { runId, toolCallId })`. Surfaces `background-task-suspended` / `background-task-resumed` chunks on `backgroundTaskManager.stream()` and `agent.streamUntilIdle().fullStream`.

client-sdks/client-js/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,6 +1922,8 @@ export class MastraClient extends BaseResource {
19221922
if (params.runId) searchParams.set('runId', params.runId);
19231923
if (params.threadId) searchParams.set('threadId', params.threadId);
19241924
if (params.resourceId) searchParams.set('resourceId', params.resourceId);
1925+
if (params.toolName) searchParams.set('toolName', params.toolName);
1926+
if (params.toolCallId) searchParams.set('toolCallId', params.toolCallId);
19251927
if (params.fromDate) searchParams.set('fromDate', params.fromDate.toISOString());
19261928
if (params.toDate) searchParams.set('toDate', params.toDate.toISOString());
19271929
if (params.dateFilterBy) searchParams.set('dateFilterBy', params.dateFilterBy);

client-sdks/client-js/src/resources/agent.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,6 +2142,72 @@ export class Agent extends BaseResource {
21422142
return streamResponse;
21432143
}
21442144

2145+
/**
2146+
* Resumes a suspended agent stream until idle with custom resume data.
2147+
* Used to continue execution after a suspension point (e.g., workflow suspend within an agent).
2148+
*/
2149+
async resumeStreamUntilIdle<OUTPUT extends {}>(
2150+
resumeData: JSONValue,
2151+
options: ResumeStreamParams<OUTPUT> & {
2152+
maxIdleMs?: number;
2153+
},
2154+
): Promise<
2155+
Response & {
2156+
processDataStream: ({
2157+
onChunk,
2158+
}: {
2159+
onChunk: Parameters<typeof processMastraStream>[0]['onChunk'];
2160+
}) => Promise<void>;
2161+
}
2162+
> {
2163+
const processedParams = {
2164+
...options,
2165+
resumeData,
2166+
requestContext: parseClientRequestContext(options.requestContext),
2167+
clientTools: processClientTools(options.clientTools),
2168+
structuredOutput: options.structuredOutput
2169+
? {
2170+
...options.structuredOutput,
2171+
schema: standardSchemaToJSONSchema(toStandardSchema(options.structuredOutput.schema)),
2172+
}
2173+
: undefined,
2174+
};
2175+
2176+
let readableController: ReadableStreamDefaultController<Uint8Array>;
2177+
const readable = new ReadableStream<Uint8Array>({
2178+
start(controller) {
2179+
readableController = controller;
2180+
},
2181+
});
2182+
2183+
const response = await this.processStreamResponse(processedParams, readableController!, 'resume-stream-until-idle');
2184+
2185+
const streamResponse = new Response(readable, {
2186+
status: response.status,
2187+
statusText: response.statusText,
2188+
headers: response.headers,
2189+
}) as Response & {
2190+
processDataStream: ({
2191+
onChunk,
2192+
}: {
2193+
onChunk: Parameters<typeof processMastraStream>[0]['onChunk'];
2194+
}) => Promise<void>;
2195+
};
2196+
2197+
streamResponse.processDataStream = async ({
2198+
onChunk,
2199+
}: {
2200+
onChunk: Parameters<typeof processMastraStream>[0]['onChunk'];
2201+
}) => {
2202+
await processMastraStream({
2203+
stream: streamResponse.body as ReadableStream<Uint8Array>,
2204+
onChunk,
2205+
});
2206+
};
2207+
2208+
return streamResponse;
2209+
}
2210+
21452211
/**
21462212
* Approves a pending tool call and returns the complete response (non-streaming).
21472213
* Used when `requireToolApproval` is enabled with generate() to allow the agent to proceed.

client-sdks/client-js/src/route-types.generated.ts

Lines changed: 183 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5845,6 +5845,176 @@ export interface PostAgentsAgentIdDeclineNetworkToolCall_RouteContract {
58455845
responseType: 'stream';
58465846
}
58475847

5848+
// ============================================================================
5849+
// Route: POST /agents/:agentId/resume-stream-until-idle
5850+
// ============================================================================
5851+
export type PostAgentsAgentIdResumeStreamUntilIdle_PathParams = {
5852+
/** Unique identifier for the agent */
5853+
agentId: string;
5854+
};
5855+
5856+
export type PostAgentsAgentIdResumeStreamUntilIdle_Body = {
5857+
instructions?: (string | string[] | any | any[]) | undefined;
5858+
system?: (string | string[] | any | any[]) | undefined;
5859+
context?: any[] | undefined;
5860+
memory?:
5861+
| {
5862+
thread:
5863+
| string
5864+
| {
5865+
id: string;
5866+
[x: string]: unknown;
5867+
};
5868+
resource: string;
5869+
options?:
5870+
| {
5871+
[key: string]: any;
5872+
}
5873+
| undefined;
5874+
readOnly?: boolean | undefined;
5875+
}
5876+
| undefined;
5877+
runId: string;
5878+
savePerStep?: boolean | undefined;
5879+
requestContext?:
5880+
| {
5881+
[key: string]: any;
5882+
}
5883+
| undefined;
5884+
versions?:
5885+
| {
5886+
agents?:
5887+
| {
5888+
[key: string]:
5889+
| {
5890+
versionId: string;
5891+
}
5892+
| {
5893+
status: 'draft' | 'published';
5894+
};
5895+
}
5896+
| undefined;
5897+
}
5898+
| undefined;
5899+
maxSteps?: number | undefined;
5900+
stopWhen?: any | undefined;
5901+
providerOptions?:
5902+
| {
5903+
anthropic?:
5904+
| {
5905+
[key: string]: any;
5906+
}
5907+
| undefined;
5908+
google?:
5909+
| {
5910+
[key: string]: any;
5911+
}
5912+
| undefined;
5913+
openai?:
5914+
| {
5915+
[key: string]: any;
5916+
}
5917+
| undefined;
5918+
xai?:
5919+
| {
5920+
[key: string]: any;
5921+
}
5922+
| undefined;
5923+
}
5924+
| undefined;
5925+
modelSettings?: any | undefined;
5926+
activeTools?: string[] | undefined;
5927+
toolsets?:
5928+
| {
5929+
[key: string]: any;
5930+
}
5931+
| undefined;
5932+
clientTools?:
5933+
| {
5934+
[key: string]: any;
5935+
}
5936+
| undefined;
5937+
toolChoice?:
5938+
| (
5939+
| ('auto' | 'none' | 'required')
5940+
| {
5941+
type: 'tool';
5942+
toolName: string;
5943+
}
5944+
)
5945+
| undefined;
5946+
requireToolApproval?: boolean | undefined;
5947+
scorers?:
5948+
| (
5949+
| {
5950+
[key: string]: any;
5951+
}
5952+
| {
5953+
[key: string]: {
5954+
scorer: string;
5955+
sampling?: any | undefined;
5956+
};
5957+
}
5958+
)
5959+
| undefined;
5960+
returnScorerData?: boolean | undefined;
5961+
tracingOptions?:
5962+
| {
5963+
metadata?:
5964+
| {
5965+
[key: string]: unknown;
5966+
}
5967+
| undefined;
5968+
requestContextKeys?: string[] | undefined;
5969+
traceId?: string | undefined;
5970+
parentSpanId?: string | undefined;
5971+
tags?: string[] | undefined;
5972+
hideInput?: boolean | undefined;
5973+
hideOutput?: boolean | undefined;
5974+
}
5975+
| undefined;
5976+
output?: any | undefined;
5977+
structuredOutput?:
5978+
| {
5979+
schema: {
5980+
[x: string]: unknown;
5981+
};
5982+
model?: (string | any) | undefined;
5983+
instructions?: string | undefined;
5984+
jsonPromptInjection?: boolean | undefined;
5985+
errorStrategy?: ('strict' | 'warn' | 'fallback') | undefined;
5986+
fallbackValue?: any | undefined;
5987+
}
5988+
| undefined;
5989+
resumeData: unknown;
5990+
toolCallId?: string | undefined;
5991+
maxIdleMs?: number | undefined;
5992+
[x: string]: unknown;
5993+
};
5994+
5995+
export type PostAgentsAgentIdResumeStreamUntilIdle_Response = any;
5996+
5997+
export type PostAgentsAgentIdResumeStreamUntilIdle_Request = Simplify<
5998+
(PostAgentsAgentIdResumeStreamUntilIdle_PathParams extends never
5999+
? {}
6000+
: { params: PostAgentsAgentIdResumeStreamUntilIdle_PathParams }) &
6001+
(never extends never ? {} : {} extends never ? { query?: never } : { query: never }) &
6002+
(PostAgentsAgentIdResumeStreamUntilIdle_Body extends never
6003+
? {}
6004+
: {} extends PostAgentsAgentIdResumeStreamUntilIdle_Body
6005+
? { body?: PostAgentsAgentIdResumeStreamUntilIdle_Body }
6006+
: { body: PostAgentsAgentIdResumeStreamUntilIdle_Body })
6007+
>;
6008+
6009+
export interface PostAgentsAgentIdResumeStreamUntilIdle_RouteContract {
6010+
pathParams: PostAgentsAgentIdResumeStreamUntilIdle_PathParams;
6011+
queryParams: never;
6012+
body: PostAgentsAgentIdResumeStreamUntilIdle_Body;
6013+
request: PostAgentsAgentIdResumeStreamUntilIdle_Request;
6014+
response: PostAgentsAgentIdResumeStreamUntilIdle_Response;
6015+
responseType: 'stream';
6016+
}
6017+
58486018
// ============================================================================
58496019
// Route: POST /agents/:agentId/network
58506020
// ============================================================================
@@ -74364,14 +74534,16 @@ export interface GetBackgroundTasksStream_RouteContract {
7436474534
// ============================================================================
7436574535
export type GetBackgroundTasks_QueryParams = {
7436674536
agentId?: string | undefined;
74367-
status?: ('pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timed_out') | undefined;
74537+
status?: ('pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled' | 'timed_out') | undefined;
7436874538
runId?: string | undefined;
7436974539
threadId?: string | undefined;
7437074540
resourceId?: string | undefined;
74541+
toolName?: string | undefined;
74542+
toolCallId?: string | undefined;
7437174543
fromDate?: Date | undefined;
7437274544
toDate?: Date | undefined;
74373-
dateFilterBy?: ('createdAt' | 'startedAt' | 'completedAt') | undefined;
74374-
orderBy?: ('createdAt' | 'startedAt' | 'completedAt') | undefined;
74545+
dateFilterBy?: ('createdAt' | 'startedAt' | 'suspendedAt' | 'completedAt') | undefined;
74546+
orderBy?: ('createdAt' | 'startedAt' | 'suspendedAt' | 'completedAt') | undefined;
7437574547
orderDirection?: ('asc' | 'desc') | undefined;
7437674548
page?: number | undefined;
7437774549
perPage?: number | undefined;
@@ -74380,7 +74552,7 @@ export type GetBackgroundTasks_QueryParams = {
7438074552
export type GetBackgroundTasks_Response = {
7438174553
tasks: {
7438274554
id: string;
74383-
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timed_out';
74555+
status: 'pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled' | 'timed_out';
7438474556
toolName: string;
7438574557
toolCallId: string;
7438674558
args: {
@@ -74403,6 +74575,7 @@ export type GetBackgroundTasks_Response = {
7440374575
retryCount: number;
7440474576
maxRetries: number;
7440574577
timeoutMs: number;
74578+
suspendPayload?: unknown | undefined;
7440674579
}[];
7440774580
total: number;
7440874581
};
@@ -74435,7 +74608,7 @@ export type GetBackgroundTasksBackgroundTaskId_PathParams = {
7443574608

7443674609
export type GetBackgroundTasksBackgroundTaskId_Response = {
7443774610
id: string;
74438-
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timed_out';
74611+
status: 'pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled' | 'timed_out';
7443974612
toolName: string;
7444074613
toolCallId: string;
7444174614
args: {
@@ -74458,6 +74631,7 @@ export type GetBackgroundTasksBackgroundTaskId_Response = {
7445874631
retryCount: number;
7445974632
maxRetries: number;
7446074633
timeoutMs: number;
74634+
suspendPayload?: unknown | undefined;
7446174635
};
7446274636

7446374637
export type GetBackgroundTasksBackgroundTaskId_Request = Simplify<
@@ -75042,6 +75216,7 @@ export interface RouteTypes {
7504275216
'POST /agents/:agentId/decline-tool-call-generate': PostAgentsAgentIdDeclineToolCallGenerate_RouteContract;
7504375217
'POST /agents/:agentId/approve-network-tool-call': PostAgentsAgentIdApproveNetworkToolCall_RouteContract;
7504475218
'POST /agents/:agentId/decline-network-tool-call': PostAgentsAgentIdDeclineNetworkToolCall_RouteContract;
75219+
'POST /agents/:agentId/resume-stream-until-idle': PostAgentsAgentIdResumeStreamUntilIdle_RouteContract;
7504575220
'POST /agents/:agentId/network': PostAgentsAgentIdNetwork_RouteContract;
7504675221
'POST /agents/:agentId/model': PostAgentsAgentIdModel_RouteContract;
7504775222
'POST /agents/:agentId/model/reset': PostAgentsAgentIdModelReset_RouteContract;
@@ -75462,6 +75637,9 @@ export interface Client {
7546275637
'/agents/:agentId/resume-stream': {
7546375638
POST: PostAgentsAgentIdResumeStream_RouteContract;
7546475639
};
75640+
'/agents/:agentId/resume-stream-until-idle': {
75641+
POST: PostAgentsAgentIdResumeStreamUntilIdle_RouteContract;
75642+
};
7546575643
'/agents/:agentId/skills/:skillName': {
7546675644
GET: GetAgentsAgentIdSkillsSkillName_RouteContract;
7546775645
};

client-sdks/client-js/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2514,7 +2514,14 @@ export interface DeletePromptBlockVersionResponse {
25142514
message: string;
25152515
}
25162516

2517-
export type BackgroundTaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timed_out';
2517+
export type BackgroundTaskStatus =
2518+
| 'pending'
2519+
| 'running'
2520+
| 'suspended'
2521+
| 'completed'
2522+
| 'failed'
2523+
| 'cancelled'
2524+
| 'timed_out';
25182525

25192526
export type BackgroundTaskDateColumn = 'createdAt' | 'startedAt' | 'completedAt';
25202527

0 commit comments

Comments
 (0)