feat(sdk): add bulk replay to api and sdk (#4105)

## Summary

Adds SDK and API support for run bulk actions. You can now create bulk
cancel or replay actions from `@trigger.dev/sdk` using run IDs or the
same filters as `runs.list()`, then retrieve, list, poll, or abort the
action by its `bulk_` handle.

Tests, docs, changesets added.

## Design

The dashboard bulk action service now accepts structured filters instead
of reading directly from a dashboard request, so the dashboard and API
share the same creation path. Replay actions created through the API are
attributed with the existing `api` trigger source, while
dashboard-created actions keep `dashboard`.

The SDK exposes the new surface under `runs.bulk.*`, including
`targetRegion` for replay region overrides and cursor pagination for
listing bulk actions.

## Filters and runIds

Nuance on filters. If `filter` is provided, it MUST have at least one
key. This is to remove the footgun of passing no filter and selecting
all runs.

```typescript
   { action: "cancel", runIds: ["run_1"] } // valid
   { action: "cancel", runIds: [] } // invalid, min(1)
   { action: "cancel", filter: { status: "FAILED" } } // valid
   { action: "cancel", filter: {} } // invalid
   { action: "cancel", filter: {}, runIds: ["run_1"] } // invalid
```
This commit is contained in:
Chris Arderne
2026-07-07 15:43:30 +01:00
committed by GitHub
parent d59743bd35
commit aa74e68c71
21 changed files with 1765 additions and 56 deletions
@@ -0,0 +1,182 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { ApiClient } from "./index.js";
type ReceivedRequest = {
method: string;
url: string;
headers: IncomingMessage["headers"];
body: string;
};
type RequestHandler = (request: ReceivedRequest, response: ServerResponse) => void | Promise<void>;
describe("ApiClient bulk actions", () => {
let server: Server;
let baseUrl: string;
let receivedRequests: ReceivedRequest[] = [];
let requestHandler: RequestHandler | undefined;
beforeEach(async () => {
receivedRequests = [];
requestHandler = undefined;
server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", async () => {
const received = {
method: req.method ?? "",
url: req.url ?? "",
headers: req.headers,
body: Buffer.concat(chunks).toString(),
} satisfies ReceivedRequest;
receivedRequests.push(received);
try {
if (requestHandler) {
await requestHandler(received, res);
} else {
json(res, { error: "No handler" }, 500);
}
} catch (error) {
json(res, { error: error instanceof Error ? error.message : String(error) }, 500);
}
});
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => {
const address = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${address.port}`;
resolve();
});
});
});
afterEach(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it("posts the exact create bulk action request body", async () => {
requestHandler = (_request, response) => json(response, { id: "bulk_created" });
const client = new ApiClient(baseUrl, "tr_test_key");
const result = await client.createBulkAction({
action: "replay",
filter: { status: ["FAILED"], taskIdentifier: "my-task" },
name: "Replay failures",
targetRegion: "eu_1",
});
expect(result).toEqual({ id: "bulk_created" });
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]?.method).toBe("POST");
expect(receivedRequests[0]?.url).toBe("/api/v1/bulk-actions");
expect(receivedRequests[0]?.headers.authorization).toBe("Bearer tr_test_key");
expect(JSON.parse(receivedRequests[0]?.body ?? "{}")).toEqual({
action: "replay",
filter: { status: ["FAILED"], taskIdentifier: "my-task" },
name: "Replay failures",
targetRegion: "eu_1",
});
});
it("lists bulk actions with cursor pagination params and parses dates", async () => {
const createdAt = "2026-07-01T10:00:00.000Z";
const completedAt = "2026-07-01T10:05:00.000Z";
requestHandler = (_request, response) =>
json(response, {
data: [
{
id: "bulk_listed",
name: "Cancel queued runs",
type: "CANCEL",
status: "COMPLETED",
counts: { total: 3, success: 2, failure: 1 },
createdAt,
completedAt,
},
],
pagination: { next: "cursor_next", previous: "cursor_previous" },
});
const client = new ApiClient(baseUrl, "tr_test_key");
const page = await client.listBulkActions({ limit: 2, after: "cursor_after" });
expect(receivedRequests[0]?.method).toBe("GET");
const url = new URL(receivedRequests[0]?.url ?? "", baseUrl);
expect(url.pathname).toBe("/api/v1/bulk-actions");
expect(url.searchParams.get("page[size]")).toBe("2");
expect(url.searchParams.get("page[after]")).toBe("cursor_after");
expect(page.pagination).toEqual({ next: "cursor_next", previous: "cursor_previous" });
expect(page.data[0]?.createdAt).toEqual(new Date(createdAt));
expect(page.data[0]?.completedAt).toEqual(new Date(completedAt));
});
it("auto-paginates bulk action lists", async () => {
requestHandler = (request, response) => {
const url = new URL(request.url, baseUrl);
if (!url.searchParams.has("page[after]")) {
return json(response, {
data: [bulkActionObject("bulk_first")],
pagination: { next: "cursor_next" },
});
}
expect(url.searchParams.get("page[after]")).toBe("cursor_next");
return json(response, {
data: [bulkActionObject("bulk_second")],
pagination: {},
});
};
const client = new ApiClient(baseUrl, "tr_test_key");
const ids: string[] = [];
for await (const bulkAction of client.listBulkActions({ limit: 1 })) {
ids.push(bulkAction.id);
}
expect(ids).toEqual(["bulk_first", "bulk_second"]);
expect(receivedRequests).toHaveLength(2);
});
it("retrieves a bulk action by id", async () => {
requestHandler = (_request, response) => json(response, bulkActionObject("bulk_retrieve"));
const client = new ApiClient(baseUrl, "tr_test_key");
const bulkAction = await client.retrieveBulkAction("bulk_retrieve");
expect(receivedRequests[0]?.method).toBe("GET");
expect(receivedRequests[0]?.url).toBe("/api/v1/bulk-actions/bulk_retrieve");
expect(bulkAction.id).toBe("bulk_retrieve");
});
it("aborts a bulk action by id", async () => {
requestHandler = (_request, response) => json(response, { id: "bulk_abort" });
const client = new ApiClient(baseUrl, "tr_test_key");
const result = await client.abortBulkAction("bulk_abort");
expect(receivedRequests[0]?.method).toBe("POST");
expect(receivedRequests[0]?.url).toBe("/api/v1/bulk-actions/bulk_abort/abort");
expect(result).toEqual({ id: "bulk_abort" });
});
});
function json(response: ServerResponse, body: unknown, status = 200) {
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(body));
}
function bulkActionObject(id: string) {
return {
id,
type: "REPLAY",
status: "PENDING",
counts: { total: 1, success: 0, failure: 0 },
createdAt: "2026-07-01T10:00:00.000Z",
};
}
+65
View File
@@ -36,7 +36,10 @@ import {
type UpdateScheduleOptions,
type UpdateSessionRequestBody,
type WaitForDurationRequestBody,
AbortBulkActionResponseBody,
ApiDeploymentListResponseItem,
BulkActionObject,
CreateBulkActionResponseBody,
AppendToStreamResponseBody,
BatchTaskRunExecutionResult,
BatchTriggerTaskV3Response,
@@ -118,8 +121,10 @@ import {
type SSEStreamPart,
} from "./runStream.js";
import type {
CreateBulkActionOptions,
CreateEnvironmentVariableParams,
ImportEnvironmentVariablesParams,
ListBulkActionsQueryParams,
ListProjectRunsQueryParams,
ListRunsQueryParams,
ListWaitpointTokensQueryParams,
@@ -141,9 +146,11 @@ export type CreateBatchApiResponse = Prettify<
>;
export type {
CreateBulkActionOptions,
CreateEnvironmentVariableParams,
ImportEnvironmentVariablesParams,
RealtimeRunSkipColumns,
ListBulkActionsQueryParams,
SubscribeToRunsQueryParams,
UpdateEnvironmentVariableParams,
};
@@ -738,6 +745,64 @@ export class ApiClient {
);
}
createBulkAction(options: CreateBulkActionOptions, requestOptions?: ZodFetchOptions) {
return zodfetch(
CreateBulkActionResponseBody,
`${this.baseUrl}/api/v1/bulk-actions`,
{
method: "POST",
headers: this.#getHeaders(false),
body: JSON.stringify(options),
},
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
);
}
listBulkActions(
query?: ListBulkActionsQueryParams,
requestOptions?: ZodFetchOptions
): CursorPagePromise<typeof BulkActionObject> {
return zodfetchCursorPage(
BulkActionObject,
`${this.baseUrl}/api/v1/bulk-actions`,
{
query: new URLSearchParams(),
limit: query?.limit,
after: query?.after,
before: query?.before,
},
{
method: "GET",
headers: this.#getHeaders(false),
},
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
);
}
retrieveBulkAction(bulkActionId: string, requestOptions?: ZodFetchOptions) {
return zodfetch(
BulkActionObject,
`${this.baseUrl}/api/v1/bulk-actions/${bulkActionId}`,
{
method: "GET",
headers: this.#getHeaders(false),
},
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
);
}
abortBulkAction(bulkActionId: string, requestOptions?: ZodFetchOptions) {
return zodfetch(
AbortBulkActionResponseBody,
`${this.baseUrl}/api/v1/bulk-actions/${bulkActionId}/abort`,
{
method: "POST",
headers: this.#getHeaders(false),
},
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
);
}
resetIdempotencyKey(
taskIdentifier: string,
idempotencyKey: string,
+36
View File
@@ -70,6 +70,42 @@ export interface ListProjectRunsQueryParams extends CursorPageParams, ListRunsQu
env?: Array<"dev" | "staging" | "prod"> | "dev" | "staging" | "prod";
}
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = T &
{
[K in Keys]-?: Required<Pick<T, K>>;
}[Keys];
/** Same filters as runs.list(), excluding pagination. */
export type BulkActionFilter = RequireAtLeastOne<Omit<ListRunsQueryParams, keyof CursorPageParams>>;
export type BulkActionSelection =
| { filter: BulkActionFilter; runIds?: never }
| { runIds: string[]; filter?: never };
type BaseBulkActionOptions = BulkActionSelection & {
name?: string;
};
type TargetRegionOption = {
/** Region identifier to replay runs in. When omitted, each replay keeps the original run's region. */
targetRegion?: string;
};
export type CreateBulkActionOptions =
| (BaseBulkActionOptions & {
action: "cancel";
targetRegion?: never;
})
| (BaseBulkActionOptions & { action: "replay" } & TargetRegionOption);
export type CreateBulkCancelActionOptions = BaseBulkActionOptions & {
targetRegion?: never;
};
export type CreateBulkReplayActionOptions = BaseBulkActionOptions & TargetRegionOption;
export type ListBulkActionsQueryParams = CursorPageParams;
export interface SubscribeToRunsQueryParams {
tasks?: Array<string> | string;
tags?: Array<string> | string;
+113
View File
@@ -9,6 +9,7 @@ import {
} from "./common.js";
import { BackgroundWorkerMetadata } from "./resources.js";
import { DequeuedMessage, MachineResources } from "./runEngine.js";
import { QueueTypeName } from "./queues.js";
export const RunEngineVersion = z.union([z.literal("V1"), z.literal("V2")]);
@@ -1223,6 +1224,118 @@ export const ListRunResponse = z.object({
export type ListRunResponse = z.infer<typeof ListRunResponse>;
const StringOrStringArray = z.union([z.string(), z.array(z.string())]);
const MachineOrMachineArray = z.union([MachinePresetName, z.array(MachinePresetName)]);
const QueueOrQueueArray = z.union([QueueTypeName, z.array(QueueTypeName)]);
const DateOrNumber = z.union([z.coerce.date(), z.number()]);
const BulkActionFilterRequestBody = z
.object({
status: z.union([RunStatus, z.array(RunStatus)]).optional(),
taskIdentifier: StringOrStringArray.optional(),
version: StringOrStringArray.optional(),
from: DateOrNumber.optional(),
to: DateOrNumber.optional(),
period: z.string().optional(),
bulkAction: z.string().optional(),
tag: StringOrStringArray.optional(),
schedule: z.string().optional(),
isTest: z.boolean().optional(),
batch: z.string().optional(),
queue: QueueOrQueueArray.optional(),
machine: MachineOrMachineArray.optional(),
region: StringOrStringArray.optional(),
})
.refine((filter) => Object.values(filter).some(isNonEmptyBulkActionFilterValue), {
message: "At least one filter must be provided",
});
/** Recursively checks for at least one non-undefined, non-empty value. */
function isNonEmptyBulkActionFilterValue(value: unknown): boolean {
if (value === undefined) {
return false;
}
if (Array.isArray(value)) {
return value.some(isNonEmptyBulkActionFilterValue);
}
if (typeof value === "string") {
return value.trim().length > 0;
}
return true;
}
const BulkActionSelectionRequestBody = {
filter: BulkActionFilterRequestBody.optional(),
runIds: z.array(z.string()).min(1).optional(),
name: z.string().max(255, "Name must be less than 255 characters").optional(),
};
export const CreateBulkActionRequestBody = z
.discriminatedUnion("action", [
z.object({
action: z.literal("cancel"),
targetRegion: z.never().optional(),
...BulkActionSelectionRequestBody,
}),
z.object({
action: z.literal("replay"),
targetRegion: z.string().optional(),
...BulkActionSelectionRequestBody,
}),
])
.refine((body) => (body.filter ? 1 : 0) + (body.runIds ? 1 : 0) === 1, {
message: "Exactly one of filter or runIds must be provided",
});
export type CreateBulkActionRequestBody = z.infer<typeof CreateBulkActionRequestBody>;
export const BulkActionStatus = z.enum(["PENDING", "COMPLETED", "ABORTED"]);
export type BulkActionStatus = z.infer<typeof BulkActionStatus>;
export const BulkActionType = z.enum(["CANCEL", "REPLAY"]);
export type BulkActionType = z.infer<typeof BulkActionType>;
export const BulkActionObject = z.object({
id: z.string(),
name: z.string().optional(),
type: BulkActionType,
status: BulkActionStatus,
counts: z.object({
total: z.number(),
success: z.number(),
failure: z.number(),
}),
createdAt: z.coerce.date(),
completedAt: z.coerce.date().optional(),
});
export type BulkActionObject = z.infer<typeof BulkActionObject>;
export const CreateBulkActionResponseBody = z.object({
id: z.string(),
});
export type CreateBulkActionResponseBody = z.infer<typeof CreateBulkActionResponseBody>;
export const AbortBulkActionResponseBody = z.object({
id: z.string(),
});
export type AbortBulkActionResponseBody = z.infer<typeof AbortBulkActionResponseBody>;
export const ListBulkActionsResponseBody = z.object({
data: z.array(BulkActionObject),
pagination: z.object({
next: z.string().optional(),
previous: z.string().optional(),
}),
});
export type ListBulkActionsResponseBody = z.infer<typeof ListBulkActionsResponseBody>;
export const CreateEnvironmentVariableRequestBody = z.object({
name: z.string(),
value: z.string(),
+1
View File
@@ -55,6 +55,7 @@ export {
type AnyRealtimeRun,
type RetrieveRunResult,
type AnyRetrieveRunResult,
type BulkAction,
} from "./runs.js";
export * as schedules from "./schedules/index.js";
export {
@@ -0,0 +1,181 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { apiClientManager } from "@trigger.dev/core/v3";
import { runs } from "./runs.js";
type ReceivedRequest = {
method: string;
url: string;
headers: IncomingMessage["headers"];
body: string;
};
type RequestHandler = (request: ReceivedRequest, response: ServerResponse) => void | Promise<void>;
describe("runs.bulk", () => {
let server: Server;
let baseUrl: string;
let receivedRequests: ReceivedRequest[] = [];
let requestHandler: RequestHandler | undefined;
beforeEach(async () => {
receivedRequests = [];
requestHandler = undefined;
server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", async () => {
const received = {
method: req.method ?? "",
url: req.url ?? "",
headers: req.headers,
body: Buffer.concat(chunks).toString(),
} satisfies ReceivedRequest;
receivedRequests.push(received);
try {
if (requestHandler) {
await requestHandler(received, res);
} else {
json(res, { error: "No handler" }, 500);
}
} catch (error) {
json(res, { error: error instanceof Error ? error.message : String(error) }, 500);
}
});
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => {
const address = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${address.port}`;
resolve();
});
});
});
afterEach(async () => {
apiClientManager.disable();
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it("creates a cancel bulk action", async () => {
requestHandler = (_request, response) => json(response, { id: "bulk_cancel" });
const result = await withApiClient(() =>
runs.bulk.cancel({ runIds: ["run_1", "run_2"], name: "Cancel selected" })
);
expect(result).toEqual({ id: "bulk_cancel" });
expect(receivedRequests[0]?.method).toBe("POST");
expect(receivedRequests[0]?.url).toBe("/api/v1/bulk-actions");
expect(JSON.parse(receivedRequests[0]?.body ?? "{}")).toEqual({
action: "cancel",
runIds: ["run_1", "run_2"],
name: "Cancel selected",
});
});
it("creates a replay bulk action", async () => {
requestHandler = (_request, response) => json(response, { id: "bulk_replay" });
const result = await withApiClient(() =>
runs.bulk.replay({
filter: { status: "FAILED", taskIdentifier: ["task-a", "task-b"] },
name: "Replay failed tasks",
targetRegion: "eu_1",
})
);
expect(result).toEqual({ id: "bulk_replay" });
expect(receivedRequests[0]?.method).toBe("POST");
expect(receivedRequests[0]?.url).toBe("/api/v1/bulk-actions");
expect(JSON.parse(receivedRequests[0]?.body ?? "{}")).toEqual({
action: "replay",
filter: { status: "FAILED", taskIdentifier: ["task-a", "task-b"] },
name: "Replay failed tasks",
targetRegion: "eu_1",
});
});
it("retrieves and aborts bulk actions", async () => {
requestHandler = (request, response) => {
if (request.method === "GET") {
return json(response, bulkActionObject("bulk_read", "PENDING"));
}
return json(response, { id: "bulk_read" });
};
const retrieved = await withApiClient(() => runs.bulk.retrieve("bulk_read"));
const aborted = await withApiClient(() => runs.bulk.abort("bulk_read"));
expect(retrieved.id).toBe("bulk_read");
expect(retrieved.createdAt).toEqual(new Date("2026-07-01T10:00:00.000Z"));
expect(aborted).toEqual({ id: "bulk_read" });
expect(receivedRequests.map((request) => `${request.method} ${request.url}`)).toEqual([
"GET /api/v1/bulk-actions/bulk_read",
"POST /api/v1/bulk-actions/bulk_read/abort",
]);
});
it("lists bulk actions", async () => {
requestHandler = (_request, response) =>
json(response, {
data: [bulkActionObject("bulk_listed", "COMPLETED")],
pagination: { next: "cursor_next" },
});
const page = await withApiClient(() => runs.bulk.list({ limit: 1, before: "cursor_before" }));
const url = new URL(receivedRequests[0]?.url ?? "", baseUrl);
expect(receivedRequests[0]?.method).toBe("GET");
expect(url.pathname).toBe("/api/v1/bulk-actions");
expect(url.searchParams.get("page[size]")).toBe("1");
expect(url.searchParams.get("page[before]")).toBe("cursor_before");
expect(page.data[0]?.id).toBe("bulk_listed");
expect(page.pagination.next).toBe("cursor_next");
});
it("polls until the bulk action finishes", async () => {
requestHandler = (_request, response) => {
const status = receivedRequests.length === 1 ? "PENDING" : "COMPLETED";
return json(response, bulkActionObject("bulk_poll", status));
};
const bulkAction = await withApiClient(() =>
runs.bulk.poll("bulk_poll", { pollIntervalMs: 1 })
);
expect(bulkAction.status).toBe("COMPLETED");
expect(receivedRequests.map((request) => request.url)).toEqual([
"/api/v1/bulk-actions/bulk_poll",
"/api/v1/bulk-actions/bulk_poll",
]);
});
function withApiClient<T>(fn: () => Promise<T>) {
return apiClientManager.runWithConfig(
{ baseURL: baseUrl, accessToken: "tr_test_key" },
async () => fn()
);
}
});
function json(response: ServerResponse, body: unknown, status = 200) {
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(body));
}
function bulkActionObject(id: string, status: "PENDING" | "COMPLETED" | "ABORTED") {
return {
id,
type: "REPLAY",
status,
counts: { total: 2, success: status === "COMPLETED" ? 2 : 0, failure: 0 },
createdAt: "2026-07-01T10:00:00.000Z",
completedAt: status === "COMPLETED" ? "2026-07-01T10:05:00.000Z" : undefined,
};
}
+148
View File
@@ -2,7 +2,10 @@ import type {
AnyRetrieveRunResult,
AnyRunShape,
ApiRequestOptions,
CreateBulkCancelActionOptions,
CreateBulkReplayActionOptions,
InferRunTypes,
ListBulkActionsQueryParams,
ListProjectRunsQueryParams,
ListRunsQueryParams,
RescheduleRunRequestBody,
@@ -16,7 +19,10 @@ import type {
AsyncIterableStream,
ApiPromise,
RealtimeRunSkipColumns,
AbortBulkActionResponseBody,
BulkActionObject,
CanceledRunResponse,
CreateBulkActionResponseBody,
CursorPagePromise,
ListRunResponseItem,
ReplayRunResponse,
@@ -49,6 +55,14 @@ export const runs = {
retrieve: retrieveRun,
list: listRuns,
reschedule: rescheduleRun,
bulk: {
cancel: bulkCancelRuns,
replay: bulkReplayRuns,
retrieve: retrieveBulkAction,
abort: abortBulkAction,
list: listBulkActions,
poll: pollBulkAction,
},
poll,
subscribeToRun,
subscribeToRunsWithTag,
@@ -57,6 +71,7 @@ export const runs = {
};
export type ListRunsItem = ListRunResponseItem;
export type BulkAction = BulkActionObject;
function listRuns(
projectRef: string,
@@ -278,6 +293,139 @@ function cancelRun(
return apiClient.cancelRun(runId, $requestOptions);
}
function bulkCancelRuns(
options: CreateBulkCancelActionOptions,
requestOptions?: ApiRequestOptions
): ApiPromise<CreateBulkActionResponseBody> {
const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
tracer,
name: "runs.bulk.cancel()",
icon: "runs",
attributes: {
...flattenAttributes(options as Record<string, unknown>, "bulkAction"),
},
},
requestOptions
);
return apiClient.createBulkAction({ ...options, action: "cancel" }, $requestOptions);
}
function bulkReplayRuns(
options: CreateBulkReplayActionOptions,
requestOptions?: ApiRequestOptions
): ApiPromise<CreateBulkActionResponseBody> {
const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
tracer,
name: "runs.bulk.replay()",
icon: "runs",
attributes: {
...flattenAttributes(options as Record<string, unknown>, "bulkAction"),
},
},
requestOptions
);
return apiClient.createBulkAction({ ...options, action: "replay" }, $requestOptions);
}
function retrieveBulkAction(
bulkActionId: string,
requestOptions?: ApiRequestOptions
): ApiPromise<BulkActionObject> {
const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
tracer,
name: "runs.bulk.retrieve()",
icon: "runs",
attributes: {
bulkActionId,
...accessoryAttributes({
items: [{ text: bulkActionId, variant: "normal" }],
style: "codepath",
}),
},
},
requestOptions
);
return apiClient.retrieveBulkAction(bulkActionId, $requestOptions);
}
function abortBulkAction(
bulkActionId: string,
requestOptions?: ApiRequestOptions
): ApiPromise<AbortBulkActionResponseBody> {
const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
tracer,
name: "runs.bulk.abort()",
icon: "runs",
attributes: {
bulkActionId,
...accessoryAttributes({
items: [{ text: bulkActionId, variant: "normal" }],
style: "codepath",
}),
},
},
requestOptions
);
return apiClient.abortBulkAction(bulkActionId, $requestOptions);
}
function listBulkActions(
params?: ListBulkActionsQueryParams,
requestOptions?: ApiRequestOptions
): CursorPagePromise<typeof BulkActionObject> {
const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
tracer,
name: "runs.bulk.list()",
icon: "runs",
attributes: {
...flattenAttributes(params as Record<string, unknown>, "queryParams"),
},
},
requestOptions
);
return apiClient.listBulkActions(params, $requestOptions);
}
async function pollBulkAction(
bulkActionId: string,
options?: { pollIntervalMs?: number },
requestOptions?: ApiRequestOptions
): Promise<BulkActionObject> {
let attempts = 0;
while (attempts++ < MAX_POLL_ATTEMPTS) {
const bulkAction = await retrieveBulkAction(bulkActionId, requestOptions);
if (bulkAction.status !== "PENDING") {
return bulkAction;
}
await new Promise((resolve) => setTimeout(resolve, options?.pollIntervalMs ?? 1000));
}
throw new Error(`Bulk action ${bulkActionId} did not finish after ${MAX_POLL_ATTEMPTS} attempts`);
}
function rescheduleRun(
runId: string,
body: RescheduleRunRequestBody,