diff --git a/apps/webapp/app/routes/resources.runs.$runParam.ts b/apps/webapp/app/routes/resources.runs.$runParam.ts
index 744dff7964..fc4f3a9b0f 100644
--- a/apps/webapp/app/routes/resources.runs.$runParam.ts
+++ b/apps/webapp/app/routes/resources.runs.$runParam.ts
@@ -72,6 +72,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
},
payload: true,
payloadType: true,
+ metadata: true,
+ metadataType: true,
maxAttempts: true,
project: {
include: {
diff --git a/apps/webapp/app/utils/packets.ts b/apps/webapp/app/utils/packets.ts
new file mode 100644
index 0000000000..42116c26e7
--- /dev/null
+++ b/apps/webapp/app/utils/packets.ts
@@ -0,0 +1,36 @@
+import { IOPacket } from "@trigger.dev/core/v3/utils/ioSerialization";
+import { env } from "~/env.server";
+import { ServiceValidationError } from "~/v3/services/baseService.server";
+
+export class MetadataTooLargeError extends ServiceValidationError {
+ constructor(message: string) {
+ super(message, 413);
+ this.name = "MetadataTooLargeError";
+ }
+}
+
+export function handleMetadataPacket(metadata: any, metadataType: string): IOPacket | undefined {
+ let metadataPacket: IOPacket | undefined = undefined;
+
+ if (typeof metadata === "string") {
+ metadataPacket = { data: metadata, dataType: metadataType };
+ }
+
+ if (metadataType === "application/json") {
+ metadataPacket = { data: JSON.stringify(metadata), dataType: "application/json" };
+ }
+
+ if (!metadataPacket || !metadataPacket.data) {
+ return;
+ }
+
+ const byteLength = Buffer.byteLength(metadataPacket.data, "utf8");
+
+ if (byteLength > env.TASK_RUN_METADATA_MAXIMUM_SIZE) {
+ throw new MetadataTooLargeError(
+ `Metadata exceeds maximum size of ${env.TASK_RUN_METADATA_MAXIMUM_SIZE} bytes`
+ );
+ }
+
+ return metadataPacket;
+}
diff --git a/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts b/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts
index 6f2eca0b56..c4d5282f30 100644
--- a/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts
+++ b/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts
@@ -9,6 +9,7 @@ import {
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
TaskRunSuccessfulExecutionResult,
+ parsePacket,
serverWebsocketMessages,
} from "@trigger.dev/core/v3";
import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
@@ -1033,6 +1034,11 @@ class SharedQueueTasks {
const machinePreset = machinePresetFromConfig(backgroundWorkerTask.machineConfig ?? {});
+ const metadata = await parsePacket({
+ data: taskRun.metadata ?? undefined,
+ dataType: taskRun.metadataType,
+ });
+
const execution: ProdTaskRunExecution = {
task: {
id: backgroundWorkerTask.slug,
@@ -1060,6 +1066,7 @@ class SharedQueueTasks {
durationMs: taskRun.usageDurationMs,
costInCents: taskRun.costInCents,
baseCostInCents: taskRun.baseCostInCents,
+ metadata,
},
queue: {
id: queue.friendlyId,
diff --git a/apps/webapp/app/v3/services/completeAttempt.server.ts b/apps/webapp/app/v3/services/completeAttempt.server.ts
index a7fb61671e..7ec8a4c03a 100644
--- a/apps/webapp/app/v3/services/completeAttempt.server.ts
+++ b/apps/webapp/app/v3/services/completeAttempt.server.ts
@@ -128,6 +128,12 @@ export class CompleteAttemptService extends BaseService {
output: completion.output,
outputType: completion.outputType,
usageDurationMs: completion.usage?.durationMs,
+ taskRun: {
+ update: {
+ output: completion.output,
+ outputType: completion.outputType,
+ },
+ },
},
});
@@ -348,6 +354,15 @@ export class CompleteAttemptService extends BaseService {
})
);
} else {
+ await this._prisma.taskRun.update({
+ where: {
+ id: taskRunAttempt.taskRunId,
+ },
+ data: {
+ error: sanitizedError,
+ },
+ });
+
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: taskRunAttempt.taskRunId,
diff --git a/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts b/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts
index e3f06d3bfe..0d7660e482 100644
--- a/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts
+++ b/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts
@@ -1,4 +1,4 @@
-import { TaskRunExecution } from "@trigger.dev/core/v3";
+import { parsePacket, TaskRunExecution } from "@trigger.dev/core/v3";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
@@ -157,6 +157,11 @@ export class CreateTaskRunAttemptService extends BaseService {
const machinePreset = machinePresetFromConfig(taskRun.lockedBy.machineConfig ?? {});
+ const metadata = await parsePacket({
+ data: taskRun.metadata ?? undefined,
+ dataType: taskRun.metadataType,
+ });
+
const execution: TaskRunExecution = {
task: {
id: taskRun.lockedBy.slug,
@@ -186,6 +191,7 @@ export class CreateTaskRunAttemptService extends BaseService {
baseCostInCents: taskRun.baseCostInCents,
maxAttempts: taskRun.maxAttempts ?? undefined,
version: taskRun.lockedBy.worker.version,
+ metadata,
},
queue: {
id: queue.friendlyId,
diff --git a/apps/webapp/app/v3/services/finalizeTaskRun.server.ts b/apps/webapp/app/v3/services/finalizeTaskRun.server.ts
index 95a5ea36c1..8b8250fb24 100644
--- a/apps/webapp/app/v3/services/finalizeTaskRun.server.ts
+++ b/apps/webapp/app/v3/services/finalizeTaskRun.server.ts
@@ -2,15 +2,11 @@ import { sanitizeError, TaskRunError } from "@trigger.dev/core/v3";
import { type Prisma, type TaskRun } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
-import {
- isFailedRunStatus,
- type FINAL_ATTEMPT_STATUSES,
- type FINAL_RUN_STATUSES,
-} from "../taskStatus";
+import { generateFriendlyId } from "../friendlyIdentifiers";
+import { FINAL_ATTEMPT_STATUSES, isFailedRunStatus, type FINAL_RUN_STATUSES } from "../taskStatus";
import { PerformTaskRunAlertsService } from "./alerts/performTaskRunAlerts.server";
import { BaseService } from "./baseService.server";
import { ResumeDependentParentsService } from "./resumeDependentParents.server";
-import { generateFriendlyId } from "../friendlyIdentifiers";
type BaseInput = {
id: string;
@@ -70,6 +66,10 @@ export class FinalizeTaskRunService extends BaseService {
await this.finalizeAttempt({ attemptStatus, error, run });
}
+ if (isFailedRunStatus(run.status)) {
+ await this.finalizeRunError(run, error);
+ }
+
//resume any dependencies
const resumeService = new ResumeDependentParentsService(this._prisma);
const result = await resumeService.call({ id: run.id });
@@ -88,6 +88,19 @@ export class FinalizeTaskRunService extends BaseService {
return run as Output;
}
+ async finalizeRunError(run: TaskRun, error?: TaskRunError) {
+ if (!error) {
+ return;
+ }
+
+ await this._prisma.taskRun.update({
+ where: { id: run.id },
+ data: {
+ error: sanitizeError(error),
+ },
+ });
+ }
+
async finalizeAttempt({
attemptStatus,
error,
diff --git a/apps/webapp/app/v3/services/replayTaskRun.server.ts b/apps/webapp/app/v3/services/replayTaskRun.server.ts
index 20f0c06183..aa577179cd 100644
--- a/apps/webapp/app/v3/services/replayTaskRun.server.ts
+++ b/apps/webapp/app/v3/services/replayTaskRun.server.ts
@@ -65,6 +65,13 @@ export class ReplayTaskRunService extends BaseService {
payloadPacketType: payloadPacket.dataType,
});
+ const metadata = existingTaskRun.metadata
+ ? await parsePacket({
+ data: existingTaskRun.metadata,
+ dataType: existingTaskRun.metadataType,
+ })
+ : undefined;
+
try {
const tags = await getTagsForRunId({
friendlyId: existingTaskRun.friendlyId,
@@ -85,6 +92,7 @@ export class ReplayTaskRunService extends BaseService {
test: existingTaskRun.isTest,
payloadType: payloadPacket.dataType,
tags: tags?.map((t) => t.name) as RunTags,
+ metadata,
},
},
{
diff --git a/apps/webapp/app/v3/services/testTask.server.ts b/apps/webapp/app/v3/services/testTask.server.ts
index 63862e32d3..cf137cf32b 100644
--- a/apps/webapp/app/v3/services/testTask.server.ts
+++ b/apps/webapp/app/v3/services/testTask.server.ts
@@ -19,6 +19,7 @@ export class TestTaskService extends BaseService {
payload: data.payload,
options: {
test: true,
+ metadata: data.metadata,
},
});
case "SCHEDULED": {
diff --git a/apps/webapp/app/v3/services/triggerTask.server.ts b/apps/webapp/app/v3/services/triggerTask.server.ts
index 177e1279ac..4ac2a23d18 100644
--- a/apps/webapp/app/v3/services/triggerTask.server.ts
+++ b/apps/webapp/app/v3/services/triggerTask.server.ts
@@ -20,6 +20,7 @@ import { logger } from "~/services/logger.server";
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
import { createTag, MAX_TAGS_PER_RUN } from "~/models/taskRunTag.server";
import { findCurrentWorkerFromEnvironment } from "../models/workerDeployment.server";
+import { handleMetadataPacket } from "~/utils/packets";
export type TriggerTaskServiceOptions = {
idempotencyKey?: string;
@@ -99,6 +100,13 @@ export class TriggerTaskService extends BaseService {
environment
);
+ const metadataPacket = body.options?.metadata
+ ? handleMetadataPacket(
+ body.options?.metadata,
+ body.options?.metadataType ?? "application/json"
+ )
+ : undefined;
+
const dependentAttempt = body.options?.dependentAttempt
? await this._prisma.taskRunAttempt.findUnique({
where: { friendlyId: body.options.dependentAttempt },
@@ -341,6 +349,8 @@ export class TriggerTaskService extends BaseService {
batchId: dependentBatchRun?.id ?? parentBatchRun?.id,
resumeParentOnCompletion: !!(dependentAttempt ?? dependentBatchRun),
depth,
+ metadata: metadataPacket?.data,
+ metadataType: metadataPacket?.dataType,
},
});
diff --git a/apps/webapp/app/v3/taskStatus.ts b/apps/webapp/app/v3/taskStatus.ts
index 2ac65fbbfc..a5147fa953 100644
--- a/apps/webapp/app/v3/taskStatus.ts
+++ b/apps/webapp/app/v3/taskStatus.ts
@@ -50,8 +50,13 @@ export const FINAL_ATTEMPT_STATUSES = [
"COMPLETED",
"FAILED",
] satisfies TaskRunAttemptStatus[];
+
export type FINAL_ATTEMPT_STATUSES = (typeof FINAL_ATTEMPT_STATUSES)[number];
+export const FAILED_ATTEMPT_STATUSES = ["FAILED", "CANCELED"] satisfies TaskRunAttemptStatus[];
+
+export type FAILED_ATTEMPT_STATUSES = (typeof FAILED_ATTEMPT_STATUSES)[number];
+
export const FREEZABLE_RUN_STATUSES: TaskRunStatus[] = ["EXECUTING", "RETRYING_AFTER_FAILURE"];
export const FREEZABLE_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["EXECUTING", "FAILED"];
diff --git a/apps/webapp/app/v3/testTask.ts b/apps/webapp/app/v3/testTask.ts
index f3a6a89581..289d519524 100644
--- a/apps/webapp/app/v3/testTask.ts
+++ b/apps/webapp/app/v3/testTask.ts
@@ -11,6 +11,26 @@ export const TestTaskData = z
} catch (e) {
console.log("parsing error", e);
+ if (e instanceof Error) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: e.message,
+ });
+ } else {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "This is invalid JSON",
+ });
+ }
+ }
+ }),
+ metadata: z.string().transform((metadata, ctx) => {
+ try {
+ const data = JSON.parse(metadata);
+ return data as any;
+ } catch (e) {
+ console.log("parsing error", e);
+
if (e instanceof Error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
diff --git a/docs/images/run-metadata.png b/docs/images/run-metadata.png
new file mode 100644
index 0000000000..756fe5eace
Binary files /dev/null and b/docs/images/run-metadata.png differ
diff --git a/docs/management/runs/update-metadata.mdx b/docs/management/runs/update-metadata.mdx
new file mode 100644
index 0000000000..8942cf80a1
--- /dev/null
+++ b/docs/management/runs/update-metadata.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update metadata"
+openapi: "v3-openapi PUT /api/v1/runs/{runId}/metadata"
+---
diff --git a/docs/mint.json b/docs/mint.json
index c5737998f8..99ff892f87 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -1,7 +1,10 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
- "openapi": ["/openapi.yml", "/v3-openapi.yaml"],
+ "openapi": [
+ "/openapi.yml",
+ "/v3-openapi.yaml"
+ ],
"api": {
"playground": {
"mode": "simple"
@@ -103,26 +106,41 @@
"navigation": [
{
"group": "Getting Started",
- "pages": ["introduction", "quick-start", "how-it-works", "upgrading-beta", "limits"]
+ "pages": [
+ "introduction",
+ "quick-start",
+ "how-it-works",
+ "upgrading-beta",
+ "limits"
+ ]
},
{
"group": "Fundamentals",
"pages": [
{
"group": "Tasks",
- "pages": ["tasks/overview", "tasks/scheduled"]
+ "pages": [
+ "tasks/overview",
+ "tasks/scheduled"
+ ]
},
"triggering",
"apikeys",
{
"group": "Configuration",
- "pages": ["config/config-file", "config/extensions/overview"]
+ "pages": [
+ "config/config-file",
+ "config/extensions/overview"
+ ]
}
]
},
{
"group": "Development",
- "pages": ["cli-dev", "run-tests"]
+ "pages": [
+ "cli-dev",
+ "run-tests"
+ ]
},
{
"group": "Deployment",
@@ -132,7 +150,9 @@
"github-actions",
{
"group": "Deployment integrations",
- "pages": ["vercel-integration"]
+ "pages": [
+ "vercel-integration"
+ ]
}
]
},
@@ -144,7 +164,13 @@
"errors-retrying",
{
"group": "Wait",
- "pages": ["wait", "wait-for", "wait-until", "wait-for-event", "wait-for-request"]
+ "pages": [
+ "wait",
+ "wait-for",
+ "wait-until",
+ "wait-for-event",
+ "wait-for-request"
+ ]
},
"queue-concurrency",
"versioning",
@@ -162,7 +188,10 @@
"management/overview",
{
"group": "Tasks API",
- "pages": ["management/tasks/trigger", "management/tasks/batch-trigger"]
+ "pages": [
+ "management/tasks/trigger",
+ "management/tasks/batch-trigger"
+ ]
},
{
"group": "Runs API",
@@ -171,7 +200,8 @@
"management/runs/retrieve",
"management/runs/replay",
"management/runs/cancel",
- "management/runs/reschedule"
+ "management/runs/reschedule",
+ "management/runs/update-metadata"
]
},
{
@@ -200,7 +230,9 @@
},
{
"group": "Projects API",
- "pages": ["management/projects/runs"]
+ "pages": [
+ "management/projects/runs"
+ ]
}
]
},
@@ -246,7 +278,11 @@
},
{
"group": "Help",
- "pages": ["community", "help-slack", "help-email"]
+ "pages": [
+ "community",
+ "help-slack",
+ "help-email"
+ ]
},
{
"group": "Frameworks",
@@ -287,11 +323,15 @@
},
{
"group": "Dashboard",
- "pages": ["guides/dashboard/creating-a-project"]
+ "pages": [
+ "guides/dashboard/creating-a-project"
+ ]
},
{
"group": "Migrations",
- "pages": ["guides/use-cases/upgrading-from-v2"]
+ "pages": [
+ "guides/use-cases/upgrading-from-v2"
+ ]
}
],
"footerSocials": {
@@ -299,4 +339,4 @@
"github": "https://github.com/triggerdotdev",
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
}
-}
+}
\ No newline at end of file
diff --git a/docs/runs/metadata.mdx b/docs/runs/metadata.mdx
new file mode 100644
index 0000000000..2f52ce1132
--- /dev/null
+++ b/docs/runs/metadata.mdx
@@ -0,0 +1,273 @@
+---
+title: "Run metadata"
+sidebarTitle: "Metadata"
+description: "Attach a small amount of data to a run and update it as the run progresses."
+---
+
+You can attach up to 4KB (4,096 bytes) of metadata to a run, which you can then access from inside the run function, via the API, and in the dashboard. You can use metadata to store additional, structured information on a run. For example, you could store your user’s full name and corresponding unique identifier from your system on every task that is associated with that user.
+
+## Usage
+
+Add metadata to a run by passing it as an object to the `trigger` function:
+
+```ts
+const handle = await myTask.trigger(
+ { message: "hello world" },
+ { metadata: { user: { name: "Eric", id: "user_1234" } } }
+);
+```
+
+Then inside your run function, you can access the metadata like this:
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ const user = metadata.get("user");
+ console.log(user.name); // "Eric"
+ console.log(user.id); // "user_1234"
+ },
+});
+```
+
+You can also update the metadata during the run:
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ // Do some work
+ await metadata.set("progress", 0.1);
+
+ // Do some more work
+ await metadata.set("progress", 0.5);
+
+ // Do even more work
+ await metadata.set("progress", 1.0);
+ },
+});
+```
+
+You can get the current metadata at any time by calling `metadata.get()` or `metadata.current()` (again, only inside a run):
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ // Get the whole metadata object
+ const currentMetadata = metadata.current();
+ console.log(currentMetadata);
+
+ // Get a specific key
+ const user = metadata.get("user");
+ console.log(user.name); // "Eric"
+ },
+});
+```
+
+You can update metadata inside a run using `metadata.set()`, `metadata.save()`, or `metadata.del()`:
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ // Set a key
+ await metadata.set("progress", 0.5);
+
+ // Update the entire metadata object
+ await metadata.save({ progress: 0.6 });
+
+ // Delete a key
+ await metadata.del("progress");
+ },
+});
+```
+
+Any of these methods can be called anywhere "inside" the run function, or a function called from the run function:
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ await doSomeWork();
+ },
+});
+
+async function doSomeWork() {
+ await metadata.set("progress", 0.5);
+}
+```
+
+If you call any of the metadata methods outside of the run function, they will have no effect:
+
+```ts
+import { metadata } from "@trigger.dev/sdk/v3";
+
+// Somewhere outside of the run function
+async function doSomeWork() {
+ await metadata.set("progress", 0.5); // This will do nothing
+}
+```
+
+This means it's safe to call these methods anywhere in your code, and they will only have an effect when called inside the run function.
+
+
+ Calling `metadata.current()` or `metadata.get()` outside of the run function will always return
+ undefined.
+
+
+These methods also work inside any task lifecycle hook, either attached to the specific task or the global hooks defined in your `trigger.config.ts` file.
+
+
+
+```ts myTasks.ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ // Your run function work here
+ },
+ onStart: async () => {
+ await metadata.set("progress", 0.5);
+ },
+ onSuccess: async () => {
+ await metadata.set("progress", 1.0);
+ },
+});
+```
+
+```ts trigger.config.ts
+import { defineConfig, metadata } from "@trigger.dev/sdk/v3";
+
+export default defineConfig({
+ project: "proj_1234",
+ onStart: async () => {
+ await metadata.set("progress", 0.5);
+ },
+});
+```
+
+
+
+## Metadata propagation
+
+Metadata is NOT propagated to child tasks. If you want to pass metadata to a child task, you must do so explicitly:
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ await metadata.set("progress", 0.5);
+ await childTask.trigger(payload, { metadata: metadata.current() });
+ },
+});
+```
+
+## Type-safe metadata
+
+The metadata APIs are currently loosely typed, accepting any object that is JSON-serializable:
+
+```ts
+// ❌ You can't pass a top-level array
+const handle = await myTask.trigger(
+ { message: "hello world" },
+ { metadata: [{ user: { name: "Eric", id: "user_1234" } }] }
+);
+
+// ❌ You can't pass a string as the entire metadata:
+const handle = await myTask.trigger(
+ { message: "hello world" },
+ { metadata: "this is the metadata" }
+);
+
+// ❌ You can't pass in a function or a class instance
+const handle = await myTask.trigger(
+ { message: "hello world" },
+ { metadata: { user: () => "Eric", classInstance: new HelloWorld() } }
+);
+
+// ✅ You can pass in dates and other JSON-serializable objects
+const handle = await myTask.trigger(
+ { message: "hello world" },
+ { metadata: { user: { name: "Eric", id: "user_1234" }, date: new Date() } }
+);
+```
+
+
+ If you pass in an object like a Date, it will be serialized to a string when stored in the
+ metadata. That also means that when you retrieve it using `metadata.get()` or
+ `metadata.current()`, you will get a string back. You will need to deserialize it back to a Date
+ object if you need to use it as a Date.
+
+
+We recommend wrapping the metadata API in a [Zod](https://zod.dev) schema (or your validator library of choice) to provide type safety:
+
+```ts
+import { task, metadata } from "@trigger.dev/sdk/v3";
+import { z } from "zod";
+
+const Metadata = z.object({
+ user: z.object({
+ name: z.string(),
+ id: z.string(),
+ }),
+ date: z.coerce.date(), // Coerce the date string back to a Date object
+});
+
+type Metadata = z.infer;
+
+// Helper function to get the metadata object in a type-safe way
+// Note: you would probably want to use .safeParse instead of .parse in a real-world scenario
+function getMetadata() {
+ return Metadata.parse(metadata.current());
+}
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: { message: string }) => {
+ const metadata = getMetadata();
+ console.log(metadata.user.name); // "Eric"
+ console.log(metadata.user.id); // "user_1234"
+ console.log(metadata.date); // Date object
+ },
+});
+```
+
+## Inspecting metadata
+
+### Dashboard
+
+You can view the metadata for a run in the Trigger.dev dashboard. The metadata will be displayed in the run details view:
+
+
+
+### API
+
+You can use the `runs.retrieve()` SDK function to get the metadata for a run:
+
+```ts
+import { runs } from "@trigger.dev/sdk/v3";
+
+const run = await runs.retrieve("run_1234");
+
+console.log(run.metadata);
+```
+
+See the [API reference](/management/runs/retrieve) for more information.
+
+## Size limit
+
+The maximum size of the metadata object is 4KB. If you exceed this limit, the SDK will throw an error. If you are self-hosting Trigger.dev, you can increase this limit by setting the `TASK_RUN_METADATA_MAXIMUM_SIZE` environment variable. For example, to increase the limit to 16KB, you would set `TASK_RUN_METADATA_MAXIMUM_SIZE=16384`.
diff --git a/docs/tasks/overview.mdx b/docs/tasks/overview.mdx
index 37edc23232..3eaa118026 100644
--- a/docs/tasks/overview.mdx
+++ b/docs/tasks/overview.mdx
@@ -194,13 +194,14 @@ export const taskWithOnStart = task({
You can also define an `onStart` function in your `trigger.config.ts` file to get notified when any task starts.
```ts trigger.config.ts
-import type { TriggerConfig } from "@trigger.dev/sdk/v3";
+import { defineConfig } from "@trigger.dev/sdk/v3";
-export const config: TriggerConfig = {
+export default defineConfig({
+ project: "proj_1234",
onStart: async (payload, { ctx }) => {
console.log("Task started", ctx.task.id);
},
-};
+});
```
### `onSuccess` function
@@ -222,13 +223,14 @@ export const taskWithOnSuccess = task({
You can also define an `onSuccess` function in your `trigger.config.ts` file to get notified when any task succeeds.
```ts trigger.config.ts
-import type { TriggerConfig } from "@trigger.dev/sdk/v3";
+import { defineConfig } from "@trigger.dev/sdk/v3";
-export const config: TriggerConfig = {
+export default defineConfig({
+ project: "proj_1234",
onSuccess: async (payload, output, { ctx }) => {
console.log("Task succeeded", ctx.task.id);
},
-};
+});
```
### `onFailure` function
@@ -250,13 +252,14 @@ export const taskWithOnFailure = task({
You can also define an `onFailure` function in your `trigger.config.ts` file to get notified when any task fails.
```ts trigger.config.ts
-import type { TriggerConfig } from "@trigger.dev/sdk/v3";
+import { defineConfig } from "@trigger.dev/sdk/v3";
-export const config: TriggerConfig = {
+export default defineConfig({
+ project: "proj_1234",
onFailure: async (payload, error, { ctx }) => {
console.log("Task failed", ctx.task.id);
},
-};
+});
```
### `handleError` functions
diff --git a/docs/triggering.mdx b/docs/triggering.mdx
index 17c3dc32a4..7a042be562 100644
--- a/docs/triggering.mdx
+++ b/docs/triggering.mdx
@@ -220,6 +220,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json(result);
}
```
+
@@ -733,6 +734,14 @@ await myTask.trigger({ some: "data" }, { maxAttempts: 1 }); // no retries
This will override the `retry.maxAttempts` value set in the task definition.
+### `tags`
+
+View our [tags doc](/tags) for more information.
+
+### `metadata`
+
+View our [metadata doc](/runs/metadata) for more information.
+
## Large Payloads
We recommend keeping your task payloads as small as possible. We currently have a hard limit on task payloads above 10MB.
diff --git a/docs/v3-openapi.yaml b/docs/v3-openapi.yaml
index 2e141bb2d6..a557822204 100644
--- a/docs/v3-openapi.yaml
+++ b/docs/v3-openapi.yaml
@@ -360,6 +360,86 @@ paths:
const handle = await runs.replay("run_1234");
+ "/api/v1/runs/{runId}/metadata":
+ parameters:
+ - $ref: "#/components/parameters/runId"
+ put:
+ operationId: update_run_metadata_v1
+ summary: Update run metadata
+ description: Update the metadata of a run.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ metadata:
+ type: object
+ description: The new metadata to set on the run.
+ example: { key: "value" }
+ responses:
+ "200":
+ description: Successful request
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ metadata:
+ type: object
+ description: The updated metadata of the run.
+ "400":
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ enum:
+ - Invalid or missing run ID
+ - Invalid metadata
+ "401":
+ description: Unauthorized request
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ enum:
+ - Invalid or Missing API key
+ "404":
+ description: Resource not found
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ enum:
+ - Task Run not found
+ tags:
+ - runs
+ security:
+ - secretKey: []
+ x-codeSamples:
+ - lang: typescript
+ label: Update metadata
+ source: |-
+ import { metadata, task } from "@trigger.dev/sdk/v3";
+
+ export const myTask = task({
+ id: "my-task",
+ run: async () => {
+ await metadata.update({ key: "value" });
+ }
+ });
+
"/api/v2/runs/{runId}/cancel":
parameters:
- $ref: "#/components/parameters/runId"
@@ -1897,6 +1977,10 @@ components:
items:
type: string
description: A tag must be between 1 and 64 characters, a run can have up to 5 tags attached to it.
+ metadata:
+ type: object
+ description: The metadata of the run. See [Metadata](/runs/metadata) for more information.
+ example: { "foo": "bar" }
costInCents:
type: number
example: 0.00292
diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts
index 7a91915684..7b117102ae 100644
--- a/packages/core/src/v3/apiClient/index.ts
+++ b/packages/core/src/v3/apiClient/index.ts
@@ -22,6 +22,8 @@ import {
TriggerTaskRequestBody,
TriggerTaskResponse,
UpdateEnvironmentVariableRequestBody,
+ UpdateMetadataRequestBody,
+ UpdateMetadataResponseBody,
UpdateScheduleOptions,
} from "../schemas/index.js";
import { taskContext } from "../task-context-api.js";
@@ -498,6 +500,23 @@ export class ApiClient {
);
}
+ updateRunMetadata(
+ runId: string,
+ body: UpdateMetadataRequestBody,
+ requestOptions?: ZodFetchOptions
+ ) {
+ return zodfetch(
+ UpdateMetadataResponseBody,
+ `${this.baseUrl}/api/v1/runs/${runId}/metadata`,
+ {
+ method: "PUT",
+ headers: this.#getHeaders(false),
+ body: JSON.stringify(body),
+ },
+ mergeRequestOptions(this.defaultRequestOptions, requestOptions)
+ );
+ }
+
#getHeaders(spanParentAsLink: boolean) {
const headers: Record = {
"Content-Type": "application/json",
diff --git a/packages/core/src/v3/apiClientManager/index.ts b/packages/core/src/v3/apiClientManager/index.ts
index 7fca5499ab..a0aa0402a1 100644
--- a/packages/core/src/v3/apiClientManager/index.ts
+++ b/packages/core/src/v3/apiClientManager/index.ts
@@ -5,6 +5,13 @@ import { ApiClientConfiguration } from "./types.js";
const API_NAME = "api-client";
+export class ApiClientMissingError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "ApiClientMissingError";
+ }
+}
+
export class APIClientManagerAPI {
private static _instance?: APIClientManagerAPI;
@@ -44,7 +51,29 @@ export class APIClientManagerAPI {
return new ApiClient(this.baseURL, this.accessToken);
}
+ clientOrThrow(): ApiClient {
+ if (!this.baseURL || !this.accessToken) {
+ throw new ApiClientMissingError(this.apiClientMissingError());
+ }
+
+ return new ApiClient(this.baseURL, this.accessToken);
+ }
+
#getConfig(): ApiClientConfiguration | undefined {
return getGlobal(API_NAME);
}
+
+ apiClientMissingError() {
+ const hasBaseUrl = !!this.baseURL;
+ const hasAccessToken = !!this.accessToken;
+ if (!hasBaseUrl && !hasAccessToken) {
+ return `You need to set the TRIGGER_API_URL and TRIGGER_SECRET_KEY environment variables. See https://trigger.dev/docs/management/overview#authentication`;
+ } else if (!hasBaseUrl) {
+ return `You need to set the TRIGGER_API_URL environment variable. See https://trigger.dev/docs/management/overview#authentication`;
+ } else if (!hasAccessToken) {
+ return `You need to set the TRIGGER_SECRET_KEY environment variable. See https://trigger.dev/docs/management/overview#authentication`;
+ }
+
+ return `Unknown error`;
+ }
}
diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts
index 1dbb4b47c1..61d7f3bf34 100644
--- a/packages/core/src/v3/index.ts
+++ b/packages/core/src/v3/index.ts
@@ -11,6 +11,7 @@ export * from "./runtime-api.js";
export * from "./task-context-api.js";
export * from "./apiClientManager-api.js";
export * from "./usage-api.js";
+export * from "./run-metadata-api.js";
export * from "./schemas/index.js";
export { SemanticInternalAttributes } from "./semanticInternalAttributes.js";
export * from "./task-catalog-api.js";
diff --git a/packages/core/src/v3/run-metadata-api.ts b/packages/core/src/v3/run-metadata-api.ts
new file mode 100644
index 0000000000..12bc83ca39
--- /dev/null
+++ b/packages/core/src/v3/run-metadata-api.ts
@@ -0,0 +1,5 @@
+// Split module-level variable definition into separate files to allow
+// tree-shaking on each api instance.
+import { RunMetadataAPI } from "./runMetadata/index.js";
+
+export const runMetadata = RunMetadataAPI.getInstance();
diff --git a/packages/core/src/v3/runMetadata/index.ts b/packages/core/src/v3/runMetadata/index.ts
new file mode 100644
index 0000000000..0c50370a70
--- /dev/null
+++ b/packages/core/src/v3/runMetadata/index.ts
@@ -0,0 +1,96 @@
+import { DeserializedJson } from "../../schemas/json.js";
+import { apiClientManager } from "../apiClientManager-api.js";
+import { taskContext } from "../task-context-api.js";
+import { ApiRequestOptions } from "../zodfetch.js";
+
+export class RunMetadataAPI {
+ private static _instance?: RunMetadataAPI;
+ private store: Record | undefined;
+
+ private constructor() {}
+
+ public static getInstance(): RunMetadataAPI {
+ if (!this._instance) {
+ this._instance = new RunMetadataAPI();
+ }
+
+ return this._instance;
+ }
+
+ public enterWithMetadata(metadata: Record): void {
+ this.store = metadata;
+ }
+
+ public current(): Record | undefined {
+ return this.store;
+ }
+
+ public getKey(key: string): DeserializedJson | undefined {
+ return this.store?.[key];
+ }
+
+ public async setKey(
+ key: string,
+ value: DeserializedJson,
+ requestOptions?: ApiRequestOptions
+ ): Promise {
+ const runId = taskContext.ctx?.run.id;
+
+ if (!runId) {
+ return;
+ }
+
+ const apiClient = apiClientManager.clientOrThrow();
+
+ const nextStore = {
+ ...(this.store ?? {}),
+ [key]: value,
+ };
+
+ const response = await apiClient.updateRunMetadata(
+ runId,
+ { metadata: nextStore },
+ requestOptions
+ );
+
+ this.store = response.metadata;
+ }
+
+ public async deleteKey(key: string, requestOptions?: ApiRequestOptions): Promise {
+ const runId = taskContext.ctx?.run.id;
+
+ if (!runId) {
+ return;
+ }
+
+ const apiClient = apiClientManager.clientOrThrow();
+
+ const nextStore = { ...(this.store ?? {}) };
+ delete nextStore[key];
+
+ const response = await apiClient.updateRunMetadata(
+ runId,
+ { metadata: nextStore },
+ requestOptions
+ );
+
+ this.store = response.metadata;
+ }
+
+ public async update(
+ metadata: Record,
+ requestOptions?: ApiRequestOptions
+ ): Promise {
+ const runId = taskContext.ctx?.run.id;
+
+ if (!runId) {
+ return;
+ }
+
+ const apiClient = apiClientManager.clientOrThrow();
+
+ const response = await apiClient.updateRunMetadata(runId, { metadata }, requestOptions);
+
+ this.store = response.metadata;
+ }
+}
diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts
index d29aa1396b..37fa187551 100644
--- a/packages/core/src/v3/schemas/api.ts
+++ b/packages/core/src/v3/schemas/api.ts
@@ -2,6 +2,7 @@ import { z } from "zod";
import { BackgroundWorkerMetadata } from "./resources.js";
import { QueueOptions } from "./schemas.js";
import { SerializedError } from "./common.js";
+import { DeserializedJsonSchema, SerializableJsonSchema } from "../../schemas/json.js";
export const WhoAmIResponseSchema = z.object({
userId: z.string(),
@@ -81,6 +82,8 @@ export const TriggerTaskRequestBody = z.object({
ttl: z.string().or(z.number().nonnegative().int()).optional(),
tags: RunTags.optional(),
maxAttempts: z.number().int().optional(),
+ metadata: z.any(),
+ metadataType: z.string().optional(),
})
.optional(),
});
@@ -505,6 +508,7 @@ const CommonRunFields = {
costInCents: z.number(),
baseCostInCents: z.number(),
durationMs: z.number(),
+ metadata: z.record(z.any()).optional(),
};
const RetrieveRunCommandFields = {
@@ -608,3 +612,16 @@ export const EnvironmentVariable = z.object({
export const EnvironmentVariables = z.array(EnvironmentVariable);
export type EnvironmentVariables = z.infer;
+
+export const UpdateMetadataRequestBody = z.object({
+ metadata: z.record(DeserializedJsonSchema),
+ metadataType: z.string().optional(),
+});
+
+export type UpdateMetadataRequestBody = z.infer;
+
+export const UpdateMetadataResponseBody = z.object({
+ metadata: z.record(DeserializedJsonSchema),
+});
+
+export type UpdateMetadataResponseBody = z.infer;
diff --git a/packages/core/src/v3/schemas/common.ts b/packages/core/src/v3/schemas/common.ts
index 575d7f506b..e99b7dbc9e 100644
--- a/packages/core/src/v3/schemas/common.ts
+++ b/packages/core/src/v3/schemas/common.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { DeserializedJsonSchema } from "../../schemas/json.js";
// Defaults to 0.5
export const MachineCpu = z.union([
@@ -142,6 +143,7 @@ export const TaskRun = z.object({
costInCents: z.number().default(0),
baseCostInCents: z.number().default(0),
version: z.string().optional(),
+ metadata: z.record(DeserializedJsonSchema).optional(),
});
export type TaskRun = z.infer;
@@ -221,7 +223,7 @@ export const TaskRunContext = z.object({
backgroundWorkerId: true,
backgroundWorkerTaskId: true,
}),
- run: TaskRun.omit({ payload: true, payloadType: true }),
+ run: TaskRun.omit({ payload: true, payloadType: true, metadata: true }),
queue: TaskRunExecutionQueue,
environment: TaskRunExecutionEnvironment,
organization: TaskRunExecutionOrganization,
diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts
index 1ab8689bb8..0a1ef93606 100644
--- a/packages/core/src/v3/workers/taskExecutor.ts
+++ b/packages/core/src/v3/workers/taskExecutor.ts
@@ -3,7 +3,7 @@ import { VERSION } from "../../version.js";
import { ApiError, RateLimitError } from "../apiClient/errors.js";
import { ConsoleInterceptor } from "../consoleInterceptor.js";
import { parseError, sanitizeError } from "../errors.js";
-import { TriggerConfig } from "../index.js";
+import { runMetadata, TriggerConfig } from "../index.js";
import { recordSpanException, TracingSDK } from "../otel/index.js";
import {
ServerBackgroundWorker,
@@ -26,7 +26,6 @@ import {
stringifyIO,
} from "../utils/ioSerialization.js";
import { calculateNextRetryDelay } from "../utils/retries.js";
-import { accessoryAttributes } from "../utils/styleAttributes.js";
export type TaskExecutorOptions = {
tracingSDK: TracingSDK;
@@ -73,6 +72,10 @@ export class TaskExecutor {
worker,
});
+ if (execution.run.metadata) {
+ runMetadata.enterWithMetadata(execution.run.metadata);
+ }
+
this._tracingSDK.asyncResourceDetector.resolveWithAttributes({
...taskContext.attributes,
[SemanticInternalAttributes.SDK_VERSION]: VERSION,
diff --git a/packages/database/prisma/migrations/20240925092304_add_metadata_and_output_to_task_run/migration.sql b/packages/database/prisma/migrations/20240925092304_add_metadata_and_output_to_task_run/migration.sql
new file mode 100644
index 0000000000..24df8ec3cc
--- /dev/null
+++ b/packages/database/prisma/migrations/20240925092304_add_metadata_and_output_to_task_run/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "TaskRun" ADD COLUMN "metadata" TEXT,
+ADD COLUMN "metadataType" TEXT NOT NULL DEFAULT 'application/json',
+ADD COLUMN "output" TEXT,
+ADD COLUMN "outputType" TEXT NOT NULL DEFAULT 'application/json';
diff --git a/packages/database/prisma/migrations/20240925205409_add_error_to_task_run/migration.sql b/packages/database/prisma/migrations/20240925205409_add_error_to_task_run/migration.sql
new file mode 100644
index 0000000000..71ff6b0d45
--- /dev/null
+++ b/packages/database/prisma/migrations/20240925205409_add_error_to_task_run/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "TaskRun" ADD COLUMN "error" JSONB;
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 9bfafe3ee3..edadb59e74 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -1751,6 +1751,17 @@ model TaskRun {
/// The span ID of the "trigger" span in the parent task run
parentSpanId String?
+ /// Run metadata
+ metadata String?
+ metadataType String @default("application/json")
+
+ /// Run output
+ output String?
+ outputType String @default("application/json")
+
+ /// Run error
+ error Json?
+
@@unique([runtimeEnvironmentId, taskIdentifier, idempotencyKey])
// Finding child runs
@@index([parentTaskRunId])
diff --git a/packages/trigger-sdk/src/v3/envvars.ts b/packages/trigger-sdk/src/v3/envvars.ts
index c9bbd0c109..90034ef2c1 100644
--- a/packages/trigger-sdk/src/v3/envvars.ts
+++ b/packages/trigger-sdk/src/v3/envvars.ts
@@ -14,7 +14,6 @@ import {
mergeRequestOptions,
taskContext,
} from "@trigger.dev/core/v3";
-import { apiClientMissingError } from "./shared.js";
import { tracer } from "./tracer.js";
export type { CreateEnvironmentVariableParams, ImportEnvironmentVariablesParams };
@@ -76,11 +75,7 @@ export function upload(
$params = params;
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return apiClient.importEnvVars($projectRef, $slug, $params, $requestOptions);
}
@@ -121,11 +116,7 @@ export function list(
$requestOptions
);
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return apiClient.listEnvVars($projectRef, $slug, $requestOptions);
}
@@ -187,11 +178,7 @@ export function create(
$params = params;
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return apiClient.createEnvVar($projectRef, $slug, $params, $requestOptions);
}
@@ -238,11 +225,7 @@ export function retrieve(
throw new Error("slug is required");
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return apiClient.retrieveEnvVar($projectRef, $slug, $name, $requestOptions);
}
@@ -289,11 +272,7 @@ export function del(
throw new Error("slug is required");
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return apiClient.deleteEnvVar($projectRef, $slug, $name, $requestOptions);
}
@@ -362,11 +341,7 @@ export function update(
$params = params;
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return apiClient.updateEnvVar($projectRef, $slug, $name, $params, $requestOptions);
}
diff --git a/packages/trigger-sdk/src/v3/index.ts b/packages/trigger-sdk/src/v3/index.ts
index 597961e31a..00436d1189 100644
--- a/packages/trigger-sdk/src/v3/index.ts
+++ b/packages/trigger-sdk/src/v3/index.ts
@@ -7,6 +7,7 @@ export * from "./wait.js";
export * from "./usage.js";
export * from "./idempotencyKeys.js";
export * from "./tags.js";
+export * from "./metadata.js";
export type { Context };
import type { Context } from "./shared.js";
diff --git a/packages/trigger-sdk/src/v3/metadata.ts b/packages/trigger-sdk/src/v3/metadata.ts
new file mode 100644
index 0000000000..40b091a7d2
--- /dev/null
+++ b/packages/trigger-sdk/src/v3/metadata.ts
@@ -0,0 +1,164 @@
+import { DeserializedJson } from "@trigger.dev/core";
+import {
+ accessoryAttributes,
+ ApiRequestOptions,
+ flattenAttributes,
+ mergeRequestOptions,
+ runMetadata,
+} from "@trigger.dev/core/v3";
+import { tracer } from "./tracer.js";
+
+/**
+ * Provides access to run metadata operations.
+ * @namespace
+ * @property {Function} current - Get the current run's metadata.
+ * @property {Function} get - Get a specific key from the current run's metadata.
+ * @property {Function} set - Set a key in the current run's metadata.
+ * @property {Function} del - Delete a key from the current run's metadata.
+ * @property {Function} save - Update the entire metadata object for the current run.
+ */
+
+export const metadata = {
+ current: currentMetadata,
+ get: getMetadataKey,
+ set: setMetadataKey,
+ del: deleteMetadataKey,
+ save: saveMetadata,
+};
+
+export type RunMetadata = Record;
+
+/**
+ * Returns the metadata of the current run if inside a task run.
+ * This function allows you to access the entire metadata object for the current run.
+ *
+ * @returns {RunMetadata | undefined} The current run's metadata or undefined if not in a run context.
+ *
+ * @example
+ * const currentMetadata = metadata.current();
+ * console.log(currentMetadata);
+ */
+function currentMetadata(): RunMetadata | undefined {
+ return runMetadata.current();
+}
+
+/**
+ * Get a specific key from the metadata of the current run if inside a task run.
+ *
+ * @param {string} key - The key to retrieve from the metadata.
+ * @returns {DeserializedJson | undefined} The value associated with the key, or undefined if not found or not in a run context.
+ *
+ * @example
+ * const user = metadata.get("user");
+ * console.log(user.name); // "Eric"
+ * console.log(user.id); // "user_1234"
+ */
+function getMetadataKey(key: string): DeserializedJson | undefined {
+ return runMetadata.getKey(key);
+}
+
+/**
+ * Set a key in the metadata of the current run if inside a task run.
+ * This function allows you to update or add a new key-value pair to the run's metadata.
+ *
+ * @param {string} key - The key to set in the metadata.
+ * @param {DeserializedJson} value - The value to associate with the key.
+ * @param {ApiRequestOptions} [requestOptions] - Optional API request options.
+ * @returns {Promise} A promise that resolves when the metadata is updated.
+ *
+ * @example
+ * await metadata.set("progress", 0.5);
+ */
+async function setMetadataKey(
+ key: string,
+ value: DeserializedJson,
+ requestOptions?: ApiRequestOptions
+): Promise {
+ const $requestOptions = mergeRequestOptions(
+ {
+ tracer,
+ name: "metadata.set()",
+ icon: "code-plus",
+ attributes: {
+ ...accessoryAttributes({
+ items: [
+ {
+ text: key,
+ variant: "normal",
+ },
+ ],
+ style: "codepath",
+ }),
+ ...flattenAttributes(value, key),
+ },
+ },
+ requestOptions
+ );
+
+ await runMetadata.setKey(key, value, $requestOptions);
+}
+
+/**
+ * Delete a key from the metadata of the current run if inside a task run.
+ *
+ * @param {string} key - The key to delete from the metadata.
+ * @param {ApiRequestOptions} [requestOptions] - Optional API request options.
+ * @returns {Promise} A promise that resolves when the key is deleted from the metadata.
+ *
+ * @example
+ * await metadata.del("progress");
+ */
+async function deleteMetadataKey(key: string, requestOptions?: ApiRequestOptions): Promise {
+ const $requestOptions = mergeRequestOptions(
+ {
+ tracer,
+ name: "metadata.del()",
+ icon: "code-minus",
+ attributes: {
+ ...accessoryAttributes({
+ items: [
+ {
+ text: key,
+ variant: "normal",
+ },
+ ],
+ style: "codepath",
+ }),
+ key,
+ },
+ },
+ requestOptions
+ );
+
+ await runMetadata.deleteKey(key, $requestOptions);
+}
+
+/**
+ * Update the entire metadata object for the current run if inside a task run.
+ * This function allows you to replace the entire metadata object with a new one.
+ *
+ * @param {RunMetadata} metadata - The new metadata object to set for the run.
+ * @param {ApiRequestOptions} [requestOptions] - Optional API request options.
+ * @returns {Promise} A promise that resolves when the metadata is updated.
+ *
+ * @example
+ * await metadata.save({ progress: 0.6, user: { name: "Alice", id: "user_5678" } });
+ */
+async function saveMetadata(
+ metadata: RunMetadata,
+ requestOptions?: ApiRequestOptions
+): Promise {
+ const $requestOptions = mergeRequestOptions(
+ {
+ tracer,
+ name: "metadata.save()",
+ icon: "code-plus",
+ attributes: {
+ ...flattenAttributes(metadata),
+ },
+ },
+ requestOptions
+ );
+
+ await runMetadata.update(metadata, $requestOptions);
+}
diff --git a/packages/trigger-sdk/src/v3/runs.ts b/packages/trigger-sdk/src/v3/runs.ts
index 50cd77e035..4818a4f014 100644
--- a/packages/trigger-sdk/src/v3/runs.ts
+++ b/packages/trigger-sdk/src/v3/runs.ts
@@ -18,7 +18,7 @@ import {
isRequestOptions,
mergeRequestOptions,
} from "@trigger.dev/core/v3";
-import { AnyTask, Prettify, RunHandle, Task, apiClientMissingError } from "./shared.js";
+import { AnyTask, Prettify, RunHandle, Task } from "./shared.js";
import { tracer } from "./tracer.js";
import { resolvePresignedPacketUrl } from "@trigger.dev/core/v3/utils/ioSerialization";
@@ -57,11 +57,7 @@ function listRuns(
paramsOrOptions?: ListRunsQueryParams | ListProjectRunsQueryParams | ApiRequestOptions,
requestOptions?: ApiRequestOptions
): CursorPagePromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = listRunsRequestOptions(
paramsOrProjectRef,
@@ -158,11 +154,7 @@ function retrieveRun | AnyTask | string>(
runId: RunId,
requestOptions?: ApiRequestOptions
): ApiPromise> {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -216,11 +208,7 @@ function replayRun(
runId: string,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -250,11 +238,7 @@ function cancelRun(
runId: string,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -285,11 +269,7 @@ function rescheduleRun(
body: RescheduleRunRequestBody,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
diff --git a/packages/trigger-sdk/src/v3/schedules/index.ts b/packages/trigger-sdk/src/v3/schedules/index.ts
index 6fd8785d3f..e23cf2bbf1 100644
--- a/packages/trigger-sdk/src/v3/schedules/index.ts
+++ b/packages/trigger-sdk/src/v3/schedules/index.ts
@@ -12,7 +12,7 @@ import {
taskCatalog,
} from "@trigger.dev/core/v3";
import { zodfetch } from "@trigger.dev/core/v3/zodfetch";
-import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared.js";
+import { Task, TaskOptions, createTask } from "../shared.js";
import * as SchedulesAPI from "./api.js";
import { tracer } from "../tracer.js";
@@ -86,11 +86,7 @@ export function create(
options: SchedulesAPI.CreateScheduleOptions,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -124,11 +120,7 @@ export function retrieve(
scheduleId: string,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -169,11 +161,7 @@ export function update(
options: SchedulesAPI.UpdateScheduleOptions,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -207,11 +195,7 @@ export function del(
scheduleId: string,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -245,11 +229,7 @@ export function deactivate(
scheduleId: string,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -283,11 +263,7 @@ export function activate(
scheduleId: string,
requestOptions?: ApiRequestOptions
): ApiPromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -324,11 +300,7 @@ export function list(
options?: SchedulesAPI.ListScheduleOptions,
requestOptions?: ApiRequestOptions
): OffsetLimitPagePromise {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const $requestOptions = mergeRequestOptions(
{
@@ -349,10 +321,6 @@ export function list(
export function timezones(options?: { excludeUtc?: boolean }) {
const baseUrl = apiClientManager.baseURL;
- if (!baseUrl) {
- throw apiClientMissingError();
- }
-
return zodfetch(
TimezonesResult,
`${baseUrl}/api/v1/timezones${options?.excludeUtc === true ? "?excludeUtc=true" : ""}`,
diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts
index d97cd7a1b0..e0d9fb208f 100644
--- a/packages/trigger-sdk/src/v3/shared.ts
+++ b/packages/trigger-sdk/src/v3/shared.ts
@@ -39,6 +39,7 @@ import {
import { IdempotencyKey, idempotencyKeys, isIdempotencyKey } from "./idempotencyKeys.js";
import { PollOptions, RetrieveRunResult, runs } from "./runs.js";
import { tracer } from "./tracer.js";
+import { SerializableJson } from "@trigger.dev/core";
export type Context = TaskRunContext;
@@ -496,6 +497,11 @@ export type TaskRunOptions = {
* ```
*/
tags?: RunTags;
+
+ /**
+ * Metadata to attach to the run. Metadata can be used to store additional information about the run. Limited to 4KB.
+ */
+ metadata?: Record;
};
type TaskRunConcurrencyOptions = Queue;
@@ -769,11 +775,7 @@ async function trigger_internal(
options?: TaskRunOptions,
requestOptions?: ApiRequestOptions
): Promise> {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const payloadPacket = await stringifyIO(payload);
@@ -792,6 +794,7 @@ async function trigger_internal(
tags: options?.tags,
maxAttempts: options?.maxAttempts,
parentAttempt: taskContext.ctx?.attempt.id,
+ metadata: options?.metadata,
},
},
{
@@ -837,11 +840,7 @@ async function batchTrigger_internal(
requestOptions?: ApiRequestOptions,
queue?: QueueOptions
): Promise> {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const response = await apiClient.batchTriggerTask(
id,
@@ -863,6 +862,7 @@ async function batchTrigger_internal(
tags: item.options?.tags,
maxAttempts: item.options?.maxAttempts,
parentAttempt: taskContext.ctx?.attempt.id,
+ metadata: item.options?.metadata,
},
};
})
@@ -912,11 +912,7 @@ async function triggerAndWait_internal(
throw new Error("triggerAndWait can only be used from inside a task.run()");
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const payloadPacket = await stringifyIO(payload);
@@ -939,6 +935,7 @@ async function triggerAndWait_internal(
ttl: options?.ttl,
tags: options?.tags,
maxAttempts: options?.maxAttempts,
+ metadata: options?.metadata,
},
},
{},
@@ -1006,11 +1003,7 @@ async function batchTriggerAndWait_internal(
throw new Error("batchTriggerAndWait can only be used from inside a task.run()");
}
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
return await tracer.startActiveSpan(
name,
@@ -1035,6 +1028,7 @@ async function batchTriggerAndWait_internal(
ttl: item.options?.ttl,
tags: item.options?.tags,
maxAttempts: item.options?.maxAttempts,
+ metadata: item.options?.metadata,
},
};
})
@@ -1194,20 +1188,6 @@ async function handleTaskRunExecutionResult(
}
}
-export function apiClientMissingError() {
- const hasBaseUrl = !!apiClientManager.baseURL;
- const hasAccessToken = !!apiClientManager.accessToken;
- if (!hasBaseUrl && !hasAccessToken) {
- return `You need to set the TRIGGER_API_URL and TRIGGER_SECRET_KEY environment variables.`;
- } else if (!hasBaseUrl) {
- return `You need to set the TRIGGER_API_URL environment variable.`;
- } else if (!hasAccessToken) {
- return `You need to set the TRIGGER_SECRET_KEY environment variable.`;
- }
-
- return `Unknown error`;
-}
-
async function makeKey(
idempotencyKey?: IdempotencyKey | string | string[]
): Promise {
diff --git a/packages/trigger-sdk/src/v3/tags.ts b/packages/trigger-sdk/src/v3/tags.ts
index 9b7dfb8e2a..f8f6b6feb7 100644
--- a/packages/trigger-sdk/src/v3/tags.ts
+++ b/packages/trigger-sdk/src/v3/tags.ts
@@ -7,7 +7,6 @@ import {
mergeRequestOptions,
taskContext,
} from "@trigger.dev/core/v3";
-import { apiClientMissingError } from "./shared.js";
import { tracer } from "./tracer.js";
export const tags = {
@@ -15,11 +14,7 @@ export const tags = {
};
async function addTags(tags: RunTags, requestOptions?: ApiRequestOptions) {
- const apiClient = apiClientManager.client;
-
- if (!apiClient) {
- throw apiClientMissingError();
- }
+ const apiClient = apiClientManager.clientOrThrow();
const run = taskContext.ctx?.run;
if (!run) {
diff --git a/references/v3-catalog/src/trigger/runMetadata.ts b/references/v3-catalog/src/trigger/runMetadata.ts
new file mode 100644
index 0000000000..e4f5dd8808
--- /dev/null
+++ b/references/v3-catalog/src/trigger/runMetadata.ts
@@ -0,0 +1,66 @@
+import { logger, task, metadata, AbortTaskRunError } from "@trigger.dev/sdk/v3";
+
+export const runMetadataTask = task({
+ id: "run-metadata-task",
+ run: async (payload: any) => {
+ await runMetadataChildTask.triggerAndWait(payload, {
+ metadata: {
+ hello: "world",
+ date: new Date(),
+ anotherThing: {
+ a: 1,
+ b: 2,
+ },
+ },
+ });
+ },
+});
+
+export const runMetadataChildTask = task({
+ id: "run-metadata-child-task",
+ run: async (payload: any, { ctx }) => {
+ await metadata.set("child", "task");
+
+ logger.info("metadata", { metadata: metadata.current() });
+
+ await metadata.set("child-2", "task-2");
+
+ logger.info("metadata", { current: metadata.current() });
+
+ await metadata.del("hello");
+
+ logger.info("metadata", { metadata: metadata.current() });
+
+ await metadata.save({
+ there: {
+ is: {
+ something: "here",
+ },
+ },
+ });
+
+ await runMetadataChildTask2.triggerAndWait(payload, {
+ metadata: metadata.current(),
+ });
+
+ return metadata.current();
+ },
+ onStart: async () => {
+ logger.info("metadata", { metadata: metadata.current() });
+ },
+ onSuccess: async () => {
+ logger.info("metadata", { metadata: metadata.current() });
+ },
+});
+
+export const runMetadataChildTask2 = task({
+ id: "run-metadata-child-task-2",
+ run: async (payload: any, { ctx }) => {
+ throw new AbortTaskRunError("aborting");
+ },
+});
+
+export const myTask = task({
+ id: "my-task",
+ run: async (payload: any) => {},
+});