@@ -190,8 +194,13 @@ export default function Page() {
)}
- Create
+
+ {isLoading ? "Creating…" : "Create"}
}
cancelButton={
diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts
new file mode 100644
index 000000000..1ae137975
--- /dev/null
+++ b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts
@@ -0,0 +1,102 @@
+import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
+import { AddTagsRequestBody } from "@trigger.dev/core/v3";
+import { z } from "zod";
+import { prisma } from "~/db.server";
+import { createTag, getTagsForRunId } from "~/models/taskRunTag.server";
+import { authenticateApiRequest } from "~/services/apiAuth.server";
+import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
+
+const ParamsSchema = z.object({
+ runId: z.string(),
+});
+
+export async function action({ request, params }: ActionFunctionArgs) {
+ // Ensure this is a POST request
+ if (request.method.toUpperCase() !== "POST") {
+ return { status: 405, body: "Method Not Allowed" };
+ }
+
+ // Authenticate the request
+ const authenticationResult = await authenticateApiRequest(request);
+ if (!authenticationResult) {
+ return json({ error: "Invalid or Missing API Key" }, { status: 401 });
+ }
+
+ const parsedParams = ParamsSchema.safeParse(params);
+ if (!parsedParams.success) {
+ return json(
+ { error: "Invalid request parameters", issues: parsedParams.error.issues },
+ { status: 400 }
+ );
+ }
+
+ try {
+ const anyBody = await request.json();
+
+ const body = AddTagsRequestBody.safeParse(anyBody);
+ if (!body.success) {
+ return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
+ }
+
+ const existingTags =
+ (await getTagsForRunId({
+ friendlyId: parsedParams.data.runId,
+ environmentId: authenticationResult.environment.id,
+ })) ?? [];
+
+ //remove duplicate tags from the new tags
+ const bodyTags = typeof body.data.tags === "string" ? [body.data.tags] : body.data.tags;
+ const newTags = bodyTags.filter((tag) => {
+ if (tag.trim().length === 0) return false;
+ return !existingTags.map((t) => t.name).includes(tag);
+ });
+
+ if (existingTags.length + newTags.length > 3) {
+ return json(
+ {
+ error: `Runs can only have 3 tags, you're trying to set ${
+ existingTags.length + newTags.length
+ }.`,
+ },
+ { status: 422 }
+ );
+ }
+
+ if (newTags.length === 0) {
+ return json({ message: "No new tags to add" }, { status: 200 });
+ }
+
+ //create tags
+ let tagIds: string[] = existingTags.map((t) => t.id);
+ if (newTags.length > 0) {
+ for (const tag of newTags) {
+ const tagRecord = await createTag({
+ tag,
+ projectId: authenticationResult.environment.projectId,
+ });
+ if (tagRecord) {
+ tagIds.push(tagRecord.id);
+ }
+ }
+ }
+
+ const taskRun = await prisma.taskRun.update({
+ where: {
+ friendlyId: parsedParams.data.runId,
+ runtimeEnvironmentId: authenticationResult.environment.id,
+ },
+ data: {
+ tags: {
+ connect: tagIds.map((id) => ({ id })),
+ },
+ },
+ });
+
+ return json({ message: `Successfully set ${newTags.length} new tags.` }, { status: 200 });
+ } catch (error) {
+ return json(
+ { error: error instanceof Error ? error.message : "Internal Server Error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx b/apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx
new file mode 100644
index 000000000..2f3df1d14
--- /dev/null
+++ b/apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx
@@ -0,0 +1,32 @@
+import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { z } from "zod";
+import { $replica } from "~/db.server";
+import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server";
+import { requireUserId } from "~/services/session.server";
+
+const Params = z.object({
+ projectParam: z.string(),
+});
+
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ const userId = await requireUserId(request);
+ const { projectParam } = Params.parse(params);
+
+ const project = await $replica.project.findFirst({
+ where: { slug: projectParam, deletedAt: null, organization: { members: { some: { userId } } } },
+ });
+
+ if (!project) {
+ throw new Response("Not Found", { status: 404 });
+ }
+
+ const search = new URL(request.url).searchParams;
+ const name = search.get("name");
+
+ const presenter = new RunTagListPresenter();
+ const result = await presenter.call({
+ projectId: project.id,
+ names: name ? [decodeURIComponent(name)] : undefined,
+ });
+ return result;
+}
diff --git a/apps/webapp/app/v3/services/replayTaskRun.server.ts b/apps/webapp/app/v3/services/replayTaskRun.server.ts
index 9a5baca24..50d04c26b 100644
--- a/apps/webapp/app/v3/services/replayTaskRun.server.ts
+++ b/apps/webapp/app/v3/services/replayTaskRun.server.ts
@@ -1,9 +1,10 @@
-import { conditionallyImportPacket, parsePacket } from "@trigger.dev/core/v3";
+import { conditionallyImportPacket, parsePacket, RunTags } from "@trigger.dev/core/v3";
import { TaskRun } from "@trigger.dev/database";
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
import { logger } from "~/services/logger.server";
import { BaseService } from "./baseService.server";
import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server";
+import { getTagsForRunId } from "~/models/taskRunTag.server";
export class ReplayTaskRunService extends BaseService {
public async call(existingTaskRun: TaskRun) {
@@ -36,6 +37,11 @@ export class ReplayTaskRunService extends BaseService {
});
try {
+ const tags = await getTagsForRunId({
+ friendlyId: existingTaskRun.id,
+ environmentId: authenticatedEnvironment.id,
+ });
+
const triggerTaskService = new TriggerTaskService();
return await triggerTaskService.call(
existingTaskRun.taskIdentifier,
@@ -49,6 +55,7 @@ export class ReplayTaskRunService extends BaseService {
concurrencyKey: existingTaskRun.concurrencyKey ?? undefined,
test: existingTaskRun.isTest,
payloadType: payloadPacket.dataType,
+ tags: tags?.map((t) => t.name) as RunTags,
},
},
{
diff --git a/apps/webapp/app/v3/services/triggerTask.server.ts b/apps/webapp/app/v3/services/triggerTask.server.ts
index 88c42fc59..1d4248b9f 100644
--- a/apps/webapp/app/v3/services/triggerTask.server.ts
+++ b/apps/webapp/app/v3/services/triggerTask.server.ts
@@ -17,6 +17,7 @@ import { getEntitlement } from "~/services/platform.v3.server";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { logger } from "~/services/logger.server";
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
+import { createTag } from "~/models/taskRunTag.server";
export type TriggerTaskServiceOptions = {
idempotencyKey?: string;
@@ -210,6 +211,22 @@ export class TriggerTaskService extends BaseService {
event.setAttribute("queueName", queueName);
span.setAttribute("queueName", queueName);
+ //upsert tags
+ let tagIds: string[] = [];
+ const bodyTags =
+ typeof body.options?.tags === "string" ? [body.options.tags] : body.options?.tags;
+ if (bodyTags && bodyTags.length > 0) {
+ for (const tag of bodyTags) {
+ const tagRecord = await createTag({
+ tag,
+ projectId: environment.projectId,
+ });
+ if (tagRecord) {
+ tagIds.push(tagRecord.id);
+ }
+ }
+ }
+
const taskRun = await tx.taskRun.create({
data: {
status: delayUntil ? "DELAYED" : "PENDING",
@@ -233,6 +250,12 @@ export class TriggerTaskService extends BaseService {
queuedAt: delayUntil ? undefined : new Date(),
maxAttempts: body.options?.maxAttempts,
ttl,
+ tags:
+ tagIds.length === 0
+ ? undefined
+ : {
+ connect: tagIds.map((id) => ({ id })),
+ },
},
});
diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts
index fa5553bd2..bb9fc327d 100644
--- a/packages/core/src/v3/apiClient/index.ts
+++ b/packages/core/src/v3/apiClient/index.ts
@@ -1,6 +1,8 @@
import { context, propagation } from "@opentelemetry/api";
+import { z } from "zod";
import { version } from "../../../package.json";
import {
+ AddTagsRequestBody,
BatchTaskRunExecutionResult,
BatchTriggerTaskRequestBody,
BatchTriggerTaskResponse,
@@ -63,8 +65,8 @@ const DEFAULT_ZOD_FETCH_OPTIONS: ZodFetchOptions = {
},
};
-export type { ApiRequestOptions };
export { isRequestOptions };
+export type { ApiRequestOptions };
/**
* Trigger.dev v3 API client
@@ -289,6 +291,19 @@ export class ApiClient {
);
}
+ addTags(runId: string, body: AddTagsRequestBody, requestOptions?: ZodFetchOptions) {
+ return zodfetch(
+ z.object({ message: z.string() }),
+ `${this.baseUrl}/api/v1/runs/${runId}/tags`,
+ {
+ method: "POST",
+ headers: this.#getHeaders(false),
+ body: JSON.stringify(body),
+ },
+ mergeRequestOptions(this.defaultRequestOptions, requestOptions)
+ );
+ }
+
createSchedule(options: CreateScheduleOptions, requestOptions?: ZodFetchOptions) {
return zodfetch(
ScheduleObject,
@@ -534,6 +549,13 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar
searchParams.append("filter[bulkAction]", query.bulkAction);
}
+ if (query.tag) {
+ searchParams.append(
+ "filter[tag]",
+ Array.isArray(query.tag) ? query.tag.join(",") : query.tag
+ );
+ }
+
if (query.schedule) {
searchParams.append("filter[schedule]", query.schedule);
}
diff --git a/packages/core/src/v3/apiClient/types.ts b/packages/core/src/v3/apiClient/types.ts
index c13980861..67b07d019 100644
--- a/packages/core/src/v3/apiClient/types.ts
+++ b/packages/core/src/v3/apiClient/types.ts
@@ -28,6 +28,7 @@ export interface ListRunsQueryParams extends CursorPageParams {
to?: Date | number;
period?: string;
bulkAction?: string;
+ tag?: Array | string;
schedule?: string;
isTest?: boolean;
}
diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts
index c62c22b25..9b0161614 100644
--- a/packages/core/src/v3/schemas/api.ts
+++ b/packages/core/src/v3/schemas/api.ts
@@ -55,6 +55,17 @@ export const CreateBackgroundWorkerResponse = z.object({
export type CreateBackgroundWorkerResponse = z.infer;
+//an array of 1, 2, or 3 strings
+const RunTag = z.string().max(64, "Tags must be less than 64 characters");
+export const RunTags = z.union([
+ RunTag,
+ z.tuple([RunTag]),
+ z.tuple([RunTag, RunTag]),
+ z.tuple([RunTag, RunTag, RunTag]),
+]);
+
+export type RunTags = z.infer;
+
export const TriggerTaskRequestBody = z.object({
payload: z.any(),
context: z.any(),
@@ -70,6 +81,7 @@ export const TriggerTaskRequestBody = z.object({
payloadType: z.string().optional(),
delay: z.string().or(z.coerce.date()).optional(),
ttl: z.string().or(z.number().nonnegative().int()).optional(),
+ tags: RunTags.optional(),
maxAttempts: z.number().int().optional(),
})
.optional(),
@@ -110,6 +122,12 @@ export const GetBatchResponseBody = z.object({
export type GetBatchResponseBody = z.infer;
+export const AddTagsRequestBody = z.object({
+ tags: RunTags,
+});
+
+export type AddTagsRequestBody = z.infer;
+
export const RescheduleRunRequestBody = z.object({
delay: z.string().or(z.coerce.date()),
});
@@ -452,6 +470,10 @@ const CommonRunFields = {
delayedUntil: z.coerce.date().optional(),
ttl: z.string().optional(),
expiredAt: z.coerce.date().optional(),
+ tags: z.string().array(),
+ costInCents: z.number(),
+ baseCostInCents: z.number(),
+ durationMs: z.number(),
};
export const RetrieveRunResponse = z.object({
diff --git a/packages/database/prisma/migrations/20240720101649_added_task_run_tag_removed_task_tag/migration.sql b/packages/database/prisma/migrations/20240720101649_added_task_run_tag_removed_task_tag/migration.sql
new file mode 100644
index 000000000..9d10ad68a
--- /dev/null
+++ b/packages/database/prisma/migrations/20240720101649_added_task_run_tag_removed_task_tag/migration.sql
@@ -0,0 +1,59 @@
+/*
+ Warnings:
+
+ - You are about to drop the `TaskTag` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_TaskRunToTaskTag` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "TaskTag" DROP CONSTRAINT "TaskTag_projectId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_TaskRunToTaskTag" DROP CONSTRAINT "_TaskRunToTaskTag_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_TaskRunToTaskTag" DROP CONSTRAINT "_TaskRunToTaskTag_B_fkey";
+
+-- DropTable
+DROP TABLE "TaskTag";
+
+-- DropTable
+DROP TABLE "_TaskRunToTaskTag";
+
+-- CreateTable
+CREATE TABLE "TaskRunTag" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "friendlyId" TEXT NOT NULL,
+ "projectId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "TaskRunTag_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_TaskRunToTaskRunTag" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TaskRunTag_friendlyId_key" ON "TaskRunTag"("friendlyId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TaskRunTag_projectId_name_key" ON "TaskRunTag"("projectId", "name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "_TaskRunToTaskRunTag_AB_unique" ON "_TaskRunToTaskRunTag"("A", "B");
+
+-- CreateIndex
+CREATE INDEX "_TaskRunToTaskRunTag_B_index" ON "_TaskRunToTaskRunTag"("B");
+
+-- AddForeignKey
+ALTER TABLE "TaskRunTag" ADD CONSTRAINT "TaskRunTag_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_A_fkey" FOREIGN KEY ("A") REFERENCES "TaskRun"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_B_fkey" FOREIGN KEY ("B") REFERENCES "TaskRunTag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/database/prisma/migrations/20240723104125_task_run_tag_name_id_index/migration.sql b/packages/database/prisma/migrations/20240723104125_task_run_tag_name_id_index/migration.sql
new file mode 100644
index 000000000..d0033e404
--- /dev/null
+++ b/packages/database/prisma/migrations/20240723104125_task_run_tag_name_id_index/migration.sql
@@ -0,0 +1,2 @@
+-- CreateIndex
+CREATE INDEX "TaskRunTag_name_id_idx" ON "TaskRunTag"("name", "id");
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 12946b227..c37a2837d 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -453,7 +453,7 @@ model Project {
backgroundWorkers BackgroundWorker[]
backgroundWorkerTasks BackgroundWorkerTask[]
taskRuns TaskRun[]
- taskTags TaskTag[]
+ runTags TaskRunTag[]
taskQueues TaskQueue[]
environmentVariables EnvironmentVariable[]
checkpoints Checkpoint[]
@@ -1641,8 +1641,9 @@ model TaskRun {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- attempts TaskRunAttempt[] @relation("attempts")
- tags TaskTag[]
+ attempts TaskRunAttempt[] @relation("attempts")
+ tags TaskRunTag[]
+
checkpoints Checkpoint[]
startedAt DateTime?
@@ -1735,6 +1736,24 @@ enum TaskRunStatus {
EXPIRED
}
+model TaskRunTag {
+ id String @id @default(cuid())
+ name String
+
+ friendlyId String @unique
+
+ runs TaskRun[]
+
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ projectId String
+
+ createdAt DateTime @default(now())
+
+ @@unique([projectId, name])
+ //Makes run filtering by tag faster
+ @@index([name, id])
+}
+
model TaskRunDependency {
id String @id @default(cuid())
@@ -1775,22 +1794,6 @@ model TaskRunNumberCounter {
@@unique([taskIdentifier, environmentId])
}
-model TaskTag {
- id String @id @default(cuid())
- name String
-
- friendlyId String @unique
-
- runs TaskRun[]
-
- project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
- projectId String
-
- createdAt DateTime @default(now())
-
- @@unique([projectId, name])
-}
-
model TaskRunAttempt {
id String @id @default(cuid())
number Int @default(0)
diff --git a/packages/trigger-sdk/src/v3/index.ts b/packages/trigger-sdk/src/v3/index.ts
index c1224949a..84b1365de 100644
--- a/packages/trigger-sdk/src/v3/index.ts
+++ b/packages/trigger-sdk/src/v3/index.ts
@@ -6,6 +6,7 @@ export * from "./tasks";
export * from "./wait";
export * from "./usage";
export * from "./idempotencyKeys";
+export * from "./tags";
export type { Context };
import type { Context } from "./shared";
diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts
index 16f58d67f..57658ee60 100644
--- a/packages/trigger-sdk/src/v3/shared.ts
+++ b/packages/trigger-sdk/src/v3/shared.ts
@@ -18,6 +18,7 @@ import {
QueueOptions,
RetryOptions,
RunFnParams,
+ RunTags,
SemanticInternalAttributes,
StartFnParams,
SuccessFnParams,
@@ -440,6 +441,21 @@ export type TaskRunOptions = {
* **Note:** Runs in development have a default `ttl` of 10 minutes. You can override this by setting the `ttl` option.
*/
ttl?: string | number;
+
+ /**
+ * Tags to attach to the run. Tags can be used to filter runs in the dashboard and using the SDK.
+ *
+ * You can set up to 3 tags per run, they must be less than 64 characters each.
+ *
+ * We recommend prefixing tags with a namespace using an underscore or colon, like `user_1234567` or `org:9876543`.
+ *
+ * @example
+ *
+ * ```ts
+ * await myTask.trigger({ foo: "bar" }, { tags: ["user:1234567", "org:9876543"] });
+ * ```
+ */
+ tags?: RunTags;
};
type TaskRunConcurrencyOptions = Queue;
@@ -485,6 +501,7 @@ export function createTask<
idempotencyKey: await makeKey(options?.idempotencyKey),
delay: options?.delay,
ttl: options?.ttl,
+ tags: options?.tags,
maxAttempts: options?.maxAttempts,
},
},
@@ -547,6 +564,7 @@ export function createTask<
idempotencyKey: await makeKey(item.options?.idempotencyKey),
delay: item.options?.delay,
ttl: item.options?.ttl,
+ tags: item.options?.tags,
maxAttempts: item.options?.maxAttempts,
},
};
@@ -616,6 +634,7 @@ export function createTask<
idempotencyKey: await makeKey(options?.idempotencyKey),
delay: options?.delay,
ttl: options?.ttl,
+ tags: options?.tags,
maxAttempts: options?.maxAttempts,
},
});
@@ -701,6 +720,7 @@ export function createTask<
idempotencyKey: await makeKey(item.options?.idempotencyKey),
delay: item.options?.delay,
ttl: item.options?.ttl,
+ tags: item.options?.tags,
maxAttempts: item.options?.maxAttempts,
},
};
@@ -866,6 +886,7 @@ export async function trigger(
idempotencyKey: await makeKey(options?.idempotencyKey),
delay: options?.delay,
ttl: options?.ttl,
+ tags: options?.tags,
maxAttempts: options?.maxAttempts,
},
},
@@ -942,6 +963,7 @@ export async function triggerAndWait(
idempotencyKey: await makeKey(options?.idempotencyKey),
delay: options?.delay,
ttl: options?.ttl,
+ tags: options?.tags,
maxAttempts: options?.maxAttempts,
},
},
@@ -1051,6 +1073,7 @@ export async function batchTrigger(
idempotencyKey: await makeKey(item.options?.idempotencyKey),
delay: item.options?.delay,
ttl: item.options?.ttl,
+ tags: item.options?.tags,
maxAttempts: item.options?.maxAttempts,
},
};
diff --git a/packages/trigger-sdk/src/v3/tags.ts b/packages/trigger-sdk/src/v3/tags.ts
new file mode 100644
index 000000000..991eaab38
--- /dev/null
+++ b/packages/trigger-sdk/src/v3/tags.ts
@@ -0,0 +1,66 @@
+import type { ApiRequestOptions, RunTags } from "@trigger.dev/core/v3";
+import {
+ UnprocessableEntityError,
+ accessoryAttributes,
+ apiClientManager,
+ logger,
+ mergeRequestOptions,
+ taskContext,
+} from "@trigger.dev/core/v3";
+import { apiClientMissingError } from "./shared";
+import { tracer } from "./tracer";
+
+export const tags = {
+ add: addTags,
+};
+
+async function addTags(tags: RunTags, requestOptions?: ApiRequestOptions) {
+ const apiClient = apiClientManager.client;
+
+ if (!apiClient) {
+ throw apiClientMissingError();
+ }
+
+ const run = taskContext.ctx?.run;
+ if (!run) {
+ throw new Error(
+ "Can't set tags outside of a run. You can trigger a task and set tags in the options."
+ );
+ }
+
+ const $requestOptions = mergeRequestOptions(
+ {
+ tracer,
+ name: "tags.set()",
+ icon: "tag",
+ attributes: {
+ ...accessoryAttributes({
+ items: [
+ {
+ text: typeof tags === "string" ? tags : tags.join(", "),
+ variant: "normal",
+ },
+ ],
+ style: "codepath",
+ }),
+ },
+ },
+ requestOptions
+ );
+
+ try {
+ await apiClient.addTags(run.id, { tags }, $requestOptions);
+ } catch (error) {
+ if (error instanceof UnprocessableEntityError) {
+ logger.error(error.message, {
+ existingTags: run.tags,
+ newTags: tags,
+ });
+ return;
+ }
+
+ logger.error("Failed to set tags", { error });
+
+ throw error;
+ }
+}
diff --git a/references/v3-catalog/src/trigger/subtasks.ts b/references/v3-catalog/src/trigger/subtasks.ts
index 7c6cf6f08..8472aa3ff 100644
--- a/references/v3-catalog/src/trigger/subtasks.ts
+++ b/references/v3-catalog/src/trigger/subtasks.ts
@@ -1,4 +1,4 @@
-import { logger, task, wait, tasks } from "@trigger.dev/sdk/v3";
+import { logger, task, wait, tasks, tags } from "@trigger.dev/sdk/v3";
import { taskWithRetries } from "./retries";
export const simpleParentTask = task({
@@ -27,6 +27,9 @@ export const simpleChildTask = task({
run: async (payload: { message: string }, { ctx }) => {
logger.log("Simple child task payload", { payload, ctx });
+ logger.log("Context tags", { tags: ctx.run.tags });
+ await tags.add("product:1");
+
await wait.for({ seconds: 10 });
},
});
diff --git a/references/v3-catalog/src/trigger/tags.ts b/references/v3-catalog/src/trigger/tags.ts
new file mode 100644
index 000000000..0cd1e764c
--- /dev/null
+++ b/references/v3-catalog/src/trigger/tags.ts
@@ -0,0 +1,81 @@
+import { RunTags } from "@trigger.dev/core/v3";
+import { logger, runs, tags, task, tasks } from "@trigger.dev/sdk/v3";
+import { simpleChildTask } from "./subtasks";
+
+type Payload = {
+ tags: RunTags;
+};
+
+export const triggerRunsWithTags = task({
+ id: "trigger-runs-with-tags",
+ run: async (payload: Payload, { ctx }) => {
+ const { id } = await simpleChildTask.trigger(
+ { message: "trigger from triggerRunsWithTags" },
+ { tags: payload.tags }
+ );
+
+ await simpleChildTask.triggerAndWait(
+ { message: "triggerAndWait from triggerRunsWithTags" },
+ { tags: payload.tags }
+ );
+
+ await simpleChildTask.batchTrigger([
+ {
+ payload: { message: "batchTrigger 1 from triggerRunsWithTags" },
+ options: { tags: payload.tags },
+ },
+ {
+ payload: { message: "batchTrigger 2 from triggerRunsWithTags" },
+ options: { tags: payload.tags },
+ },
+ ]);
+
+ const results = await simpleChildTask.batchTriggerAndWait([
+ {
+ payload: { message: "batchTriggerAndWait 1 from triggerRunsWithTags" },
+ options: { tags: payload.tags },
+ },
+ {
+ payload: { message: "batchTriggerAndWait 2 from triggerRunsWithTags" },
+ options: { tags: payload.tags },
+ },
+ ]);
+
+ await tasks.trigger(
+ "simple-child-task",
+ { message: "tasks.trigger from triggerRunsWithTags" },
+ { tags: payload.tags }
+ );
+ await tasks.triggerAndWait(
+ "simple-child-task",
+ { message: "tasks.triggerAndWait from triggerRunsWithTags" },
+ { tags: payload.tags }
+ );
+ await tasks.batchTrigger("simple-child-task", [
+ {
+ payload: { message: "tasks.batchTrigger 1 from triggerRunsWithTags" },
+ options: { tags: payload.tags },
+ },
+ {
+ payload: { message: "tasks.batchTrigger 2 from triggerRunsWithTags" },
+ options: { tags: payload.tags },
+ },
+ ]);
+
+ const run = await runs.retrieve(id);
+ logger.log("run", run);
+ logger.log("run usage", {
+ costInCents: run.costInCents,
+ baseCostInCents: run.baseCostInCents,
+ durationMs: run.durationMs,
+ });
+
+ const result2 = await runs.list({ tag: payload.tags });
+ logger.log("trigger runs ", { length: result2.data.length, data: result2.data });
+ logger.log("run usage", {
+ costInCents: result2.data[0].costInCents,
+ baseCostInCents: result2.data[0].baseCostInCents,
+ durationMs: result2.data[0].durationMs,
+ });
+ },
+});