Skip to content

Commit d4cd340

Browse files
authored
Query API and SDK (#3060)
Summary - Add API endpoint to run TRQL queries - Implement SDK function for executing queries ## SDK Added `query.execute()` which lets you query your Trigger.dev data using TRQL (Trigger Query Language) and returns results as typed JSON rows or CSV. It supports configurable scope (environment, project, or organization), time filtering via `period` or `from`/`to` ranges, and a `format` option for JSON or CSV output. ```typescript import { query } from "@trigger.dev/sdk"; import type { QueryTable } from "@trigger.dev/sdk"; // Basic untyped query const result = await query.execute("SELECT run_id, status FROM runs LIMIT 10"); // Type-safe query using QueryTable to pick specific columns const typedResult = await query.execute<QueryTable<"runs", "run_id" | "status" | "triggered_at">>( "SELECT run_id, status, triggered_at FROM runs LIMIT 10" ); typedResult.results.forEach(row => { console.log(row.run_id, row.status); // Fully typed }); // Aggregation query with inline types const stats = await query.execute<{ status: string; count: number }>( "SELECT status, COUNT(*) as count FROM runs GROUP BY status", { scope: "project", period: "30d" } ); // CSV export const csv = await query.execute( "SELECT run_id, status FROM runs", { format: "csv", period: "7d" } ); console.log(csv.results); // Raw CSV string ```
1 parent 796ad29 commit d4cd340

File tree

10 files changed

+732
-23
lines changed

10 files changed

+732
-23
lines changed

.changeset/afraid-gorillas-jump.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@trigger.dev/sdk": minor
3+
---
4+
5+
Added `query.execute()` which lets you query your Trigger.dev data using TRQL (Trigger Query Language) and returns results as typed JSON rows or CSV. It supports configurable scope (environment, project, or organization), time filtering via `period` or `from`/`to` ranges, and a `format` option for JSON or CSV output.
6+
7+
```typescript
8+
import { query } from "@trigger.dev/sdk";
9+
import type { QueryTable } from "@trigger.dev/sdk";
10+
11+
// Basic untyped query
12+
const result = await query.execute("SELECT run_id, status FROM runs LIMIT 10");
13+
14+
// Type-safe query using QueryTable to pick specific columns
15+
const typedResult = await query.execute<QueryTable<"runs", "run_id" | "status" | "triggered_at">>(
16+
"SELECT run_id, status, triggered_at FROM runs LIMIT 10"
17+
);
18+
typedResult.results.forEach(row => {
19+
console.log(row.run_id, row.status); // Fully typed
20+
});
21+
22+
// Aggregation query with inline types
23+
const stats = await query.execute<{ status: string; count: number }>(
24+
"SELECT status, COUNT(*) as count FROM runs GROUP BY status",
25+
{ scope: "project", period: "30d" }
26+
);
27+
28+
// CSV export
29+
const csv = await query.execute(
30+
"SELECT run_id, status FROM runs",
31+
{ format: "csv", period: "7d" }
32+
);
33+
console.log(csv.results); // Raw CSV string
34+
```

apps/webapp/app/components/runs/v3/RunIcon.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
InformationCircleIcon,
55
RectangleStackIcon,
66
Squares2X2Icon,
7+
TableCellsIcon,
78
TagIcon,
89
} from "@heroicons/react/20/solid";
910
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
@@ -47,8 +48,6 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
4748
) {
4849
return <TablerIcon name={spanNameIcon.iconName} className={className} />;
4950
}
50-
51-
<InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
5251
}
5352

5453
if (!name) return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
@@ -81,6 +80,8 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
8180
return <WaitpointTokenIcon className={cn(className, "text-sky-500")} />;
8281
case "function":
8382
return <FunctionIcon className={cn(className, "text-text-dimmed")} />;
83+
case "query":
84+
return <TableCellsIcon className={cn(className, "text-query")} />;
8485
//log levels
8586
case "debug":
8687
case "log":
@@ -110,7 +111,7 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
110111
case "task-hook-catchError":
111112
return <FunctionIcon className={cn(className, "text-error")} />;
112113
case "streams":
113-
return <StreamsIcon className={cn(className, "text-text-dimmed")} />;
114+
return <StreamsIcon className={cn(className, "text-text-dimmed")} />;
114115
}
115116

116117
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;

apps/webapp/app/components/runs/v3/TaskRunStatus.tsx

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
XCircleIcon,
1313
} from "@heroicons/react/20/solid";
1414
import type { TaskRunStatus } from "@trigger.dev/database";
15+
import { runFriendlyStatus, type RunFriendlyStatus } from "@trigger.dev/core/v3";
1516
import assertNever from "assert-never";
1617
import { HourglassIcon } from "lucide-react";
1718
import { TimedOutIcon } from "~/assets/icons/TimedOutIcon";
@@ -248,26 +249,9 @@ export function runStatusFromFriendlyTitle(friendly: RunFriendlyStatus): TaskRun
248249
return result[0] as TaskRunStatus;
249250
}
250251

251-
export const runFriendlyStatus = [
252-
"Delayed",
253-
"Queued",
254-
"Pending version",
255-
"Dequeued",
256-
"Executing",
257-
"Waiting",
258-
"Reattempting",
259-
"Paused",
260-
"Canceled",
261-
"Interrupted",
262-
"Completed",
263-
"Failed",
264-
"System failure",
265-
"Crashed",
266-
"Expired",
267-
"Timed out",
268-
] as const;
269-
270-
export type RunFriendlyStatus = (typeof runFriendlyStatus)[number];
252+
// runFriendlyStatus and RunFriendlyStatus are imported from @trigger.dev/core/v3
253+
// and re-exported here for backward compatibility.
254+
export { runFriendlyStatus, type RunFriendlyStatus } from "@trigger.dev/core/v3";
271255

272256
/**
273257
* Check if a value is a valid TaskRunStatus
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { json } from "@remix-run/server-runtime";
2+
import { QueryError } from "@internal/clickhouse";
3+
import { z } from "zod";
4+
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
5+
import { executeQuery, type QueryScope } from "~/services/queryService.server";
6+
import { logger } from "~/services/logger.server";
7+
import { rowsToCSV } from "~/utils/dataExport";
8+
9+
const BodySchema = z.object({
10+
query: z.string(),
11+
scope: z.enum(["organization", "project", "environment"]).default("environment"),
12+
period: z.string().nullish(),
13+
from: z.string().nullish(),
14+
to: z.string().nullish(),
15+
format: z.enum(["json", "csv"]).default("json"),
16+
});
17+
18+
const { action, loader } = createActionApiRoute(
19+
{
20+
body: BodySchema,
21+
corsStrategy: "all",
22+
},
23+
async ({ body, authentication }) => {
24+
const { query, scope, period, from, to, format } = body;
25+
const env = authentication.environment;
26+
27+
const queryResult = await executeQuery({
28+
name: "api-query",
29+
query,
30+
scope: scope as QueryScope,
31+
organizationId: env.organization.id,
32+
projectId: env.project.id,
33+
environmentId: env.id,
34+
period,
35+
from,
36+
to,
37+
history: {
38+
source: "API",
39+
},
40+
});
41+
42+
if (!queryResult.success) {
43+
const message =
44+
queryResult.error instanceof QueryError
45+
? queryResult.error.message
46+
: "An unexpected error occurred while executing the query.";
47+
48+
logger.error("Query API error", {
49+
error: queryResult.error,
50+
query,
51+
});
52+
53+
return json(
54+
{ error: message },
55+
{ status: queryResult.error instanceof QueryError ? 400 : 500 }
56+
);
57+
}
58+
59+
const { result, periodClipped, maxQueryPeriod } = queryResult;
60+
61+
if (format === "csv") {
62+
const csv = rowsToCSV(result.rows, result.columns);
63+
64+
return json({
65+
format: "csv",
66+
results: csv,
67+
});
68+
}
69+
70+
return json({
71+
format: "json",
72+
results: result.rows,
73+
});
74+
}
75+
);
76+
77+
export { action, loader };

packages/core/src/v3/apiClient/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import {
3030
ListScheduleOptions,
3131
QueueItem,
3232
QueueTypeName,
33+
QueryExecuteRequestBody,
34+
QueryExecuteResponseBody,
35+
QueryExecuteCSVResponseBody,
3336
ReplayRunResponse,
3437
RescheduleRunRequestBody,
3538
ResetIdempotencyKeyResponse,
@@ -1406,6 +1409,38 @@ export class ApiClient {
14061409
);
14071410
}
14081411

1412+
async executeQuery(
1413+
query: string,
1414+
options?: {
1415+
scope?: "environment" | "project" | "organization";
1416+
period?: string;
1417+
from?: string;
1418+
to?: string;
1419+
format?: "json" | "csv";
1420+
},
1421+
requestOptions?: ZodFetchOptions
1422+
): Promise<QueryExecuteResponseBody> {
1423+
const body = {
1424+
query,
1425+
scope: options?.scope ?? "environment",
1426+
period: options?.period,
1427+
from: options?.from,
1428+
to: options?.to,
1429+
format: options?.format ?? "json",
1430+
};
1431+
1432+
return zodfetch(
1433+
QueryExecuteResponseBody,
1434+
`${this.baseUrl}/api/v1/query`,
1435+
{
1436+
method: "POST",
1437+
headers: this.#getHeaders(false),
1438+
body: JSON.stringify(body),
1439+
},
1440+
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
1441+
);
1442+
}
1443+
14091444
#getHeaders(spanParentAsLink: boolean, additionalHeaders?: Record<string, string | undefined>) {
14101445
const headers: Record<string, string> = {
14111446
"Content-Type": "application/json",

packages/core/src/v3/schemas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from "./webhooks.js";
1515
export * from "./checkpoints.js";
1616
export * from "./warmStart.js";
1717
export * from "./queues.js";
18+
export * from "./query.js";

0 commit comments

Comments
 (0)