diff --git a/.changeset/red-bags-camp.md b/.changeset/red-bags-camp.md
new file mode 100644
index 000000000..b25776f91
--- /dev/null
+++ b/.changeset/red-bags-camp.md
@@ -0,0 +1,5 @@
+---
+"@trigger.dev/sdk": patch
+---
+
+Better handle event parsing errors from Zod
diff --git a/apps/docs/getting-started.mdx b/apps/docs/getting-started.mdx
index ed480d893..83f9bc61c 100644
--- a/apps/docs/getting-started.mdx
+++ b/apps/docs/getting-started.mdx
@@ -201,13 +201,13 @@ Triggers are what cause your workflows to run.
Easily subscribe to the APIs you're using
-
+
Trigger your workflows on a repeating schedule
-
+
More details on custom events
-
+
On received email, HTTP endpoint and AWS Event Bridge
@@ -222,11 +222,14 @@ Triggers are what cause your workflows to run.
>
We are making it easy to use lots of APIs by adding integrations.
-
+
+ Call any API from your workflow
+
+
Add delays to your workflows. They're resilient so it doesn't matter if your
server goes down.
-
+
Send an event, to trigger a custom event workflow
diff --git a/apps/docs/guides/zod.mdx b/apps/docs/guides/zod.mdx
index 8803c2f59..646d36814 100644
--- a/apps/docs/guides/zod.mdx
+++ b/apps/docs/guides/zod.mdx
@@ -1,7 +1,306 @@
---
-title: "Zod"
+title: "Zod Guide"
sidebarTitle: "Zod"
description: "TypeScript-first schema validation with static type inference"
---
-## Coming soon
+## Intro
+
+Zod is a fantastic utility package by [@colinhacks](https://twitter.com/colinhacks) that allows for defining runtime schema validation and type-safety.
+
+We use it [extensively](https://github.com/search?q=repo%3Atriggerdotdev%2Ftrigger.dev+%22zod%22%3B&type=code) internally at Trigger.dev to ensure we never trigger your workflows with invalid data (and more).
+
+But there are a few places where we ask you to provide us with a Zod schema, for example when defining a [Custom Event Trigger](/triggers/custom-events):
+
+```ts
+import { Trigger, customEvent } from "@trigger.dev/sdk";
+import { slack } from "@trigger.dev/integrations";
+import { z } from "zod";
+
+//this workflow will run when a "user.created" event is sent
+new Trigger({
+ id: "zod",
+ name: "Zod custom event",
+ //this is the custom event subscription
+ on: customEvent({
+ name: "user.created",
+ schema: z.object({
+ name: z.string(),
+ email: z.string(),
+ paidPlan: z.boolean(),
+ }),
+ }),
+ //this function is run when the custom event is received
+ run: async (event, ctx) => {
+ console.log(event); // Has name, email, paidPlan
+ },
+}).listen();
+```
+
+So it will help to know a little about Zod and how to use it. We definitely recommend the well written [Zod README](https://github.com/colinhacks/zod#readme) but we've included a short primer below.
+
+
+ Wherever we require you to pass in a Zod schema, you can always start with
+ `z.any()` which accepts `any` type and then add more strict validations later.
+
+
+## Basic Usage
+
+There are three main steps to using Zod:
+
+1. Define a schema
+
+```ts
+import { z } from "zod";
+
+const mySchema = z.object({
+ name: z.string(),
+ email: z.string(),
+ paidPlan: z.boolean(),
+});
+```
+
+2. Infer TypeScript types from the schema
+
+```ts
+type MySchema = z.infer;
+```
+
+3. Validate data against the schema
+
+```ts
+const data: unknown = {
+ name: "Eric",
+ email: "eric@trigger.dev",
+ paidPlan: true,
+};
+
+const result = mySchema.parse(data);
+```
+
+When using Zod with Trigger.dev, you'll only really need to do the first step, and by passing it to us (through the `schema` property of the `customEvent` function), we'll do the second and third steps for you.
+
+## Defining Schemas
+
+### Primitives
+
+Zod schemas are a way to define the shape of an object. They can be as simple as a single type, or as complex as a nested object.
+
+```ts
+// Primitives
+z.string();
+z.number();
+z.boolean();
+z.date();
+z.undefined();
+z.null();
+```
+
+Any schema can be marked as optional, which means the schema can be `undefined` or `null`:
+
+```ts
+z.string().optional();
+```
+
+Schemas can also be marked optional by providing a default value:
+
+```ts
+const optionalString = z.string().default("default value");
+
+const value = optionalString.parse(undefined); // value === "default value"
+```
+
+If you need to allow a value to be `null`, you can use `nullable()`:
+
+```ts
+const nullableString = z.string().nullable();
+
+const value = nullableString.parse(null);
+```
+
+You can also use Zod to coerce primites into other types. For example, you can coerce a string into a number:
+
+```ts
+const numberString = z.coerce.number();
+
+const value = numberString.parse("123"); // value === 123
+```
+
+The following primitives are supported:
+
+```ts
+z.coerce.string();
+z.coerce.number();
+z.coerce.boolean();
+z.coerce.bigint();
+z.coerce.date();
+```
+
+Coercing dates are especially useful when you are receiving a `string` from an API and want to convert it to a JavaScript `Date` object:
+
+```ts
+const date = z.coerce.date();
+
+const value = date.parse("2021-01-01T00:00:00.000Z"); // value === Date object
+```
+
+### Objects
+
+Object schemas are the most common type of schema. They allow you to define the shape of an object, and the types of each property.
+
+```ts
+const mySchema = z.object({
+ name: z.string(),
+ email: z.string(),
+ paidPlan: z.boolean(),
+});
+```
+
+All properties are required by default, although you can make them all optional using `partial()`:
+
+```ts
+const mySchema = z
+ .object({
+ name: z.string(),
+ email: z.string(),
+ paidPlan: z.boolean(),
+ })
+ .partial();
+```
+
+You can also make individual properties optional:
+
+```ts
+const mySchema = z.object({
+ name: z.string(),
+ email: z.string(),
+ paidPlan: z.boolean().optional(),
+});
+```
+
+By default an object schema will strip out any extra properties that are not defined in the schema. You can disable this behavior using `passthrough()`:
+
+```ts
+const mySchema = z.object({
+ name: z.string(),
+});
+
+mySchema.parse({ name: "Eric", email: "eric@trigger.dev" }); // { name: "Eric" }
+mySchema.passthrough().parse({ name: "Eric", email: "eric@trigger.dev" }); // { name: "Eric", email: "eric@trigger.dev" }
+```
+
+Or you can use `strict()` to make the schema throw an error if there are any extra properties:
+
+```ts
+const mySchema = z
+ .object({
+ name: z.string(),
+ })
+ .strict();
+
+mySchema.parse({ name: "Eric", email: "eric@trigger.dev" }); // throws error
+```
+
+Zod includes a few useful object schema utilities to help with reusing schemas, `extends()` and `merge()`:
+
+```ts
+const baseSchema = z.object({
+ name: z.string(),
+});
+
+const extendedSchema = baseSchema.extend({
+ email: z.string(),
+});
+
+const mergedSchema = baseSchema.merge(
+ z.object({
+ email: z.string(),
+ })
+);
+```
+
+You can also use `pick()` and `omit()` to create a new schema that only includes or excludes certain properties:
+
+```ts
+const mySchema = z.object({
+ name: z.string(),
+ email: z.string(),
+ paidPlan: z.boolean(),
+});
+
+const pickedSchema = mySchema.pick({ name: true, email: true });
+const omittedSchema = mySchema.omit({ paidPlan: true });
+```
+
+### Arrays
+
+You can specify the schema of an array using `z.array()`:
+
+```ts
+z.array(z.string()); // string[]
+z.array(z.number()); // number[]
+z.array(z.object({ name: z.string() })); // Array<{ name: string }>
+```
+
+You can also specify the type of array that has a fixed number of elements using `z.tuple()`:
+
+```ts
+z.tuple([z.string(), z.number(), z.boolean()]); // [string, number, boolean]
+```
+
+### Unions
+
+You can specify a union of schemas using `z.union()`:
+
+```ts
+z.union([z.string(), z.number(), z.boolean()]); // string | number | boolean
+```
+
+Discriminating unions are also supported, and especially useful when paired with type narrowing:
+
+```ts
+const mySchema = z.discriminatingUnion("type", [
+ z.object({
+ type: z.literal("a"),
+ data: z.string(),
+ }),
+ z.object({
+ type: z.literal("b"),
+ data: z.number(),
+ }),
+]);
+
+const value = mySchema.parse({ type: "a", data: "hello" });
+
+if (type.a) {
+ // value is { type: "a", data: string }
+} else if (type.b) {
+ // value is { type: "b", data: number }
+}
+```
+
+### Records
+
+You can specify a record of schemas using `z.record()`, useful for when you have a map of values but don't care about the keys:
+
+```ts
+z.record(z.string()); // Record
+z.record(z.number()); // Record
+```
+
+### JSON type
+
+If you want to accept any valid JSON value, you can use the following schema:
+
+```ts
+const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
+type Literal = z.infer;
+type Json = Literal | { [key: string]: Json } | Json[];
+const jsonSchema: z.ZodType = z.lazy(() =>
+ z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
+);
+
+jsonSchema.parse(data);
+```
+
+Hat tip to [ggoodman](https://github.com/ggoodman) for this one.
diff --git a/apps/docs/triggers/custom-events.mdx b/apps/docs/triggers/custom-events.mdx
index 30cd1c25b..96b793c98 100644
--- a/apps/docs/triggers/custom-events.mdx
+++ b/apps/docs/triggers/custom-events.mdx
@@ -4,27 +4,28 @@ sidebarTitle: "Custom events"
description: "Custom event triggers allow you to run workflows from your own code (or your other workflows)"
---
-[Send an event](/functions/send-event) and any workflows that subscribe to that Custom event will get triggered.
+[Send an event](/functions/send-event) and any workflows that subscribe to that custom event will get triggered.
## Name and Schemas
### Name
-Custom event triggers have a `name`. They will only get triggered when a custom even with that name are sent.
+Custom event triggers have a `name`. They will only get triggered when a custom event with that name are sent.
### Schema
-Custom event triggers have a schema. This is used to validate the data that is sent with the event. If the data does not match the schema, the workflow will not run.
+Custom event triggers take a [Zod](https://github.com/colinhacks/zod) schema. This is used to validate the data that is sent with the event. If the data does not match the schema, the workflow will not run.
-It also means that inside your run function you will have type safety for the data you receive. We use [Zod](https://github.com/colinhacks/zod#installation) for our schemas – it's a fantastic library that allows you to define schemas in a very simple way.
+It also means that inside your run function the event param will be typed correctly. We use [Zod](https://github.com/colinhacks/zod#installation) for our schemas – it's a fantastic library that allows you to define schemas in a very simple way.
-You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation.
+You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation. See our [Zod guide](/guides/zod) for more information.
## Example
```ts
import { Trigger, customEvent } from "@trigger.dev/sdk";
import { slack } from "@trigger.dev/integrations";
+import { z } from "zod";
//this workflow will run when a "user.created" event is sent
new Trigger({
diff --git a/examples/smoke-test/src/index.ts b/examples/smoke-test/src/index.ts
index 98707bff2..6d5db22cf 100644
--- a/examples/smoke-test/src/index.ts
+++ b/examples/smoke-test/src/index.ts
@@ -11,7 +11,7 @@ const trigger = new Trigger({
name: "My workflow",
apiKey: "trigger_dev_zC25mKNn6c0q",
endpoint: "ws://localhost:8889/ws",
- logLevel: "log",
+ logLevel: "debug",
triggerTTL: 60 * 60 * 24,
on: customEvent({ name: "user.created", schema: userCreatedEvent }),
run: async (event, ctx) => {
diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json
index c04c0a3e7..3939ca006 100644
--- a/packages/trigger-sdk/package.json
+++ b/packages/trigger-sdk/package.json
@@ -42,6 +42,7 @@
"ulid": "^2.3.0",
"uuid": "^9.0.0",
"ws": "^8.11.0",
- "zod": "^3.20.2"
+ "zod": "^3.20.2",
+ "zod-error": "^1.1.0"
}
}
diff --git a/packages/trigger-sdk/src/client.ts b/packages/trigger-sdk/src/client.ts
index 255c87ec8..b126dce12 100644
--- a/packages/trigger-sdk/src/client.ts
+++ b/packages/trigger-sdk/src/client.ts
@@ -14,6 +14,13 @@ import { triggerRunLocalStorage } from "./localStorage";
import { ContextLogger } from "./logger";
import { Trigger, TriggerOptions } from "./trigger";
import { TriggerContext, TriggerFetch } from "./types";
+import { generateErrorMessage, ErrorMessageOptions } from "zod-error";
+
+const zodErrorMessageOptions: ErrorMessageOptions = {
+ delimiter: {
+ error: " 🔥 ",
+ },
+};
export class TriggerClient {
#trigger: Trigger;
@@ -292,6 +299,26 @@ export class TriggerClient {
TRIGGER_WORKFLOW: async (data) => {
this.#logger.debug("Handling TRIGGER_WORKFLOW", data);
+ const parsedEventData = this.#options.on.schema.safeParse(
+ data.trigger.input
+ );
+
+ if (!parsedEventData.success) {
+ await serverRPC.send("SEND_WORKFLOW_ERROR", {
+ runId: data.id,
+ timestamp: String(highPrecisionTimestamp()),
+ error: {
+ name: "Event validation error",
+ message: generateErrorMessage(
+ parsedEventData.error.issues,
+ zodErrorMessageOptions
+ ),
+ },
+ });
+
+ return true;
+ }
+
const fetchFunction: TriggerFetch = async (key, url, options) => {
const result = new Promise((resolve, reject) => {
this.#fetchCallbacks.set(messageKey(data.id, key), {
@@ -399,7 +426,7 @@ export class TriggerClient {
fetch: fetchFunction,
};
- const eventData = this.#options.on.schema.parse(data.trigger.input);
+ const eventData = parsedEventData.data;
this.#logger.debug("Parsed event data", eventData);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 31775fa76..d848e31db 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -770,6 +770,7 @@ importers:
uuid: ^9.0.0
ws: ^8.11.0
zod: ^3.20.2
+ zod-error: ^1.1.0
dependencies:
debug: 4.3.4
evt: 2.4.13
@@ -778,6 +779,7 @@ importers:
uuid: 9.0.0
ws: 8.12.0
zod: 3.20.2
+ zod-error: 1.1.0
devDependencies:
'@trigger.dev/common-schemas': link:../common-schemas
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
@@ -16581,6 +16583,12 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ /zod-error/1.1.0:
+ resolution: {integrity: sha512-zZ0I9/eFQsaDqglBPOM5Y/kJzbTkoD6hNZGON/kcMe2WIfLse2DIwsZpLMfKwBmcciYaEg+DubtLMnAbZ4/rfA==}
+ dependencies:
+ zod: 3.20.2
+ dev: false
+
/zod/3.20.2:
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}