Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/afraid-gorillas-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@trigger.dev/sdk": minor
---

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
```
7 changes: 4 additions & 3 deletions apps/webapp/app/components/runs/v3/RunIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
InformationCircleIcon,
RectangleStackIcon,
Squares2X2Icon,
TableCellsIcon,
TagIcon,
} from "@heroicons/react/20/solid";
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
Expand Down Expand Up @@ -47,8 +48,6 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
) {
return <TablerIcon name={spanNameIcon.iconName} className={className} />;
}

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

if (!name) return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
Expand Down Expand Up @@ -81,6 +80,8 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
return <WaitpointTokenIcon className={cn(className, "text-sky-500")} />;
case "function":
return <FunctionIcon className={cn(className, "text-text-dimmed")} />;
case "query":
return <TableCellsIcon className={cn(className, "text-query")} />;
//log levels
case "debug":
case "log":
Expand Down Expand Up @@ -110,7 +111,7 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
case "task-hook-catchError":
return <FunctionIcon className={cn(className, "text-error")} />;
case "streams":
return <StreamsIcon className={cn(className, "text-text-dimmed")} />;
return <StreamsIcon className={cn(className, "text-text-dimmed")} />;
}

return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
Expand Down
24 changes: 4 additions & 20 deletions apps/webapp/app/components/runs/v3/TaskRunStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
XCircleIcon,
} from "@heroicons/react/20/solid";
import type { TaskRunStatus } from "@trigger.dev/database";
import { runFriendlyStatus, type RunFriendlyStatus } from "@trigger.dev/core/v3";
import assertNever from "assert-never";
import { HourglassIcon } from "lucide-react";
import { TimedOutIcon } from "~/assets/icons/TimedOutIcon";
Expand Down Expand Up @@ -248,26 +249,9 @@ export function runStatusFromFriendlyTitle(friendly: RunFriendlyStatus): TaskRun
return result[0] as TaskRunStatus;
}

export const runFriendlyStatus = [
"Delayed",
"Queued",
"Pending version",
"Dequeued",
"Executing",
"Waiting",
"Reattempting",
"Paused",
"Canceled",
"Interrupted",
"Completed",
"Failed",
"System failure",
"Crashed",
"Expired",
"Timed out",
] as const;

export type RunFriendlyStatus = (typeof runFriendlyStatus)[number];
// runFriendlyStatus and RunFriendlyStatus are imported from @trigger.dev/core/v3
// and re-exported here for backward compatibility.
export { runFriendlyStatus, type RunFriendlyStatus } from "@trigger.dev/core/v3";

/**
* Check if a value is a valid TaskRunStatus
Expand Down
77 changes: 77 additions & 0 deletions apps/webapp/app/routes/api.v1.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { json } from "@remix-run/server-runtime";
import { QueryError } from "@internal/clickhouse";
import { z } from "zod";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { executeQuery, type QueryScope } from "~/services/queryService.server";
import { logger } from "~/services/logger.server";
import { rowsToCSV } from "~/utils/dataExport";

const BodySchema = z.object({
query: z.string(),
scope: z.enum(["organization", "project", "environment"]).default("environment"),
period: z.string().nullish(),
from: z.string().nullish(),
to: z.string().nullish(),
format: z.enum(["json", "csv"]).default("json"),
});

const { action, loader } = createActionApiRoute(
{
body: BodySchema,
corsStrategy: "all",
},
async ({ body, authentication }) => {
const { query, scope, period, from, to, format } = body;
const env = authentication.environment;

const queryResult = await executeQuery({
name: "api-query",
query,
scope: scope as QueryScope,
organizationId: env.organization.id,
projectId: env.project.id,
environmentId: env.id,
period,
from,
to,
history: {
source: "API",
},
});

if (!queryResult.success) {
const message =
queryResult.error instanceof QueryError
? queryResult.error.message
: "An unexpected error occurred while executing the query.";

logger.error("Query API error", {
error: queryResult.error,
query,
});

return json(
{ error: message },
{ status: queryResult.error instanceof QueryError ? 400 : 500 }
);
}

const { result, periodClipped, maxQueryPeriod } = queryResult;

if (format === "csv") {
const csv = rowsToCSV(result.rows, result.columns);

return json({
format: "csv",
results: csv,
});
}

return json({
format: "json",
results: result.rows,
});
}
);

export { action, loader };
35 changes: 35 additions & 0 deletions packages/core/src/v3/apiClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import {
ListScheduleOptions,
QueueItem,
QueueTypeName,
QueryExecuteRequestBody,
QueryExecuteResponseBody,
QueryExecuteCSVResponseBody,
ReplayRunResponse,
RescheduleRunRequestBody,
ResetIdempotencyKeyResponse,
Expand Down Expand Up @@ -1406,6 +1409,38 @@ export class ApiClient {
);
}

async executeQuery(
query: string,
options?: {
scope?: "environment" | "project" | "organization";
period?: string;
from?: string;
to?: string;
format?: "json" | "csv";
},
requestOptions?: ZodFetchOptions
): Promise<QueryExecuteResponseBody> {
const body = {
query,
scope: options?.scope ?? "environment",
period: options?.period,
from: options?.from,
to: options?.to,
format: options?.format ?? "json",
};

return zodfetch(
QueryExecuteResponseBody,
`${this.baseUrl}/api/v1/query`,
{
method: "POST",
headers: this.#getHeaders(false),
body: JSON.stringify(body),
},
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
);
}

#getHeaders(spanParentAsLink: boolean, additionalHeaders?: Record<string, string | undefined>) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/v3/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from "./webhooks.js";
export * from "./checkpoints.js";
export * from "./warmStart.js";
export * from "./queues.js";
export * from "./query.js";
Loading