Compare commits

...

19 Commits

Author SHA1 Message Date
Eric Allam 45047781ff WIP Assignments 2025-08-01 15:34:15 +01:00
Eric Allam 0ca3607a96 ignore .mcp.log 2025-07-31 15:56:54 +01:00
Eric Allam df907ff0aa more mcp stuff 2025-07-31 15:56:33 +01:00
Eric Allam 32af62e124 More MCP 2025-07-31 15:55:24 +01:00
Eric Allam 44ba239762 Upgrade zod to 3.25.76 2025-07-31 15:55:24 +01:00
Eric Allam ac8aaf566d Start MCP server 2025-07-31 15:55:24 +01:00
Eric Allam 580f95a464 we now convert schema to jsonSchema on the CLI side via the indexing 2025-07-31 15:53:52 +01:00
Eric Allam def3ee0f81 WIP 2025-07-31 15:53:52 +01:00
Eric Allam 0f5647c0af Fixed some stuff 2025-07-31 15:53:52 +01:00
Eric Allam 64fc703512 Refactor JSON Schema test files for clarity
Whitespace and formatting changes were applied across the `json-schema-test` reference project to enhance code readability and cohesion. This included removing unnecessary trailing spaces and ensuring consistent indentation patterns, which improves maintainability and readability by following the project's code style guidelines.

- Renamed JSONSchema type annotations to adhere to TypeScript conventions, ensuring that all schema definitions properly satisfy the JSONSchema interface.
- Restructured some object declarations for improved clarity, especially within complex schema definitions.
- These adjustments are crucial for better future maintainability, reducing potential developer errors when interacting with these test schemas.
2025-07-31 15:53:52 +01:00
Eric Allam af1e369fe4 Add JSON schema testing and revert package dependencies
This commit introduces a comprehensive set of JSON schema testing within the monorepo, specifically adding a new test project in `references/json-schema-test`. This includes a variety of schema definitions and tasks utilizing multiple validation libraries to ensure robust type-checking and runtime validation.

Additionally, the dependency versions for `@effect/schema` have been adjusted from `^0.76.5` to `^0.75.5` to maintain compatibility across the project components. This ensures consistent behavior and compatibility with existing code bases without introducing breaking changes or unexpected behavior due to version discrepancies.

Key updates include:
- Added new test project with extensive schema validation tests.
- Ensured type safety across various task implementations.
- Reverted dependency versions to ensure compatibility.
- Created multiple schema tasks using libraries like Zod, Yup, and others for thorough testing.
2025-07-31 15:53:52 +01:00
Eric Allam 7e4e77b24a Add JSONSchema type for payloadSchema in tasks
The change was necessary to improve type safety by using a proper JSONSchema type definition instead of a generic Record<string, unknown>. This enhances the developer experience and ensures that task payloads conform to the JSON Schema Draft 7 specification. The JSONSchema type is now re-exported from the SDK for user convenience, hiding internal complexity and maintaining a seamless developer experience.

- Added JSONSchema type based on Draft 7 specification
- Updated task metadata and options to use JSONSchema type
- Hid internal schema conversion logic from users by re-exporting types from SDK
- Improved bundle safety and dependency management
2025-07-31 15:53:52 +01:00
Eric Allam 200262818f Refactor SDK to encapsulate schema-to-json package
The previous implementation required users to directly import and initialize functions from the `@trigger.dev/schema-to-json` package, which was not the intended user experience. This change refactors the SDK so that all necessary functions and types from `@trigger.dev/schema-to-json` are encapsulated within the `@trigger.dev/*` packages.

- The examples in `usage.ts` have been updated to clearly mark `@trigger.dev/schema-to-json` as an internal-only package.
- Re-export JSON Schema types and conversions in the SDK to improve developer experience (DX).
- Removed unnecessary direct dependencies on `@trigger.dev/schema-to-json` from user-facing code, ensuring initialization and conversion logic is handled internally.
- Replaced instances where users were required to manually perform schema conversions with automatic handling within the SDK for simplification and better maintainability.
2025-07-31 15:53:52 +01:00
Eric Allam 088185b05b Add JSON Schema examples using various libraries
The change introduces extensive examples of using JSON Schemas in the 'references/hello-world' project within the 'trigger.dev' repository. These examples utilize libraries like Zod, Yup, and TypeBox for JSON Schema conversion and validation. The new examples demonstrate different use cases, including automatic conversion with schemaTask, manual schema provision, and schema conversion at build time. We also updated the dependencies in 'package.json' to include the necessary libraries for schema conversion and validation.

- Included examples of processing tasks with JSON Schema using libraries such as Zod, Yup, TypeBox, and ArkType.
- Showcased schema conversion techniques and type-safe JSON Schema creation.
- Updated 'package.json' to ensure all necessary dependencies for schema operations are available.
- Created illustrative scripts that cover task management from user processing to complex schema implementations.
2025-07-31 15:53:52 +01:00
Eric Allam 74ec4063b0 Refine JSON Schema typing across packages
The changes introduce stricter typing for JSON Schema-related definitions, specifically replacing vague types with more precise ones, such as using `z.record(z.unknown())` instead of `z.any()` and `Record<string, unknown>` in place of `any`. This is part of an effort to better align with common practices and improve type safety in the packages.

- Updated the `payloadSchema` in several files to use `z.record(z.unknown())`, enhancing the type strictness and consistency with JSON Schema Draft 7 recommendations.
- Added `@types/json-schema` as a dependency, utilizing its definitions for improved type clarity and adherence to best practices in TypeScript.
- Modified various comments to explicitly mention JSON Schema Draft 7, ensuring developers are aware of the JSON Schema version being implemented.
- These adjustments are informed by research into how popular libraries and tools handle JSON Schema typing, aiming to integrate best practices for improved maintainability and interoperability.
2025-07-31 15:53:52 +01:00
Eric Allam eb81722cd5 Revise schema-to-json for bundle safety and tests
The package @trigger.dev/schema-to-json has been revised to ensure bundle safety by removing direct dependencies on schema libraries such as Zod, Yup, and Effect. This change minimizes bundle size and enhances tree-shaking by allowing external conversion libraries to be utilized only at runtime if necessary. As a result, the README was updated to reflect this usage pattern.

- Introduced `initializeSchemaConverters` function to load necessary conversion libraries at runtime, keeping the base package slim.
- Adjusted test suite to initialize converters before tests, ensuring accurate testing of schema conversion capabilities.
- Updated `schemaToJsonSchema` function to dynamically check for availability of conversion libraries, improving flexibility without increasing the package size.
- Added configuration files for Vitest to support the new testing framework, reflecting the transition from previous test setups.

These enhancements ensure that only the schema libraries actively used in an application are bundled, optimizing performance and resource usage.
2025-07-31 15:53:52 +01:00
Eric Allam 87ed9c4a67 Add support for Zod 4 in schema-to-json
This change enhances the schema-to-json package by adding support for Zod version 4, which introduces the native `toJsonSchema` method. This method facilitates a direct conversion of Zod schemas to JSON Schema format, improving performance and reducing reliance on the `zod-to-json-schema` library.

- Updated README to reflect Zod 4 support with native method and retained support for Zod 3 via existing library.
- Modified package.json to allow installation of both Zod 3 and 4 versions.
- Implemented handling for Zod 4 schemas in `src/index.ts` using their native method.
- Added a test case to verify the proper conversion of Zod 4 schemas to JSON Schema.
- Included a script for updating the package version based on the root package.json.
- Introduced a specific TypeScript config for source files.
2025-07-31 15:53:52 +01:00
Eric Allam 9b395d6a4a Refactor: Remove getSchemaToJsonSchema in favor of schemaToJsonSchema
The `getSchemaToJsonSchema` function was removed and replaced with `schemaToJsonSchema` across the codebase. This update introduces a new `@trigger.dev/schema-to-json` package to handle conversions of schema validation libraries to JSON Schema format, centralizing the functionality and improving maintainability.

- Removed `getSchemaToJsonSchema` exports and references.
- Added new schema conversion utility `@trigger.dev/schema-to-json`.
- Updated `trigger-sdk` package to utilize `schemaToJsonSchema` for payloads.
- Extensive testing coverage included to ensure conversion accuracy across various schema libraries including Zod, Yup, ArkType, Effect, and TypeBox.
- The update ensures consistent and reliable schema conversions, facilitating future enhancements and supporting additional schema libraries.
2025-07-31 15:53:52 +01:00
Eric Allam e86000b221 Add payload schema handling for task indexing
This change introduces support for handling payload schemas during task indexing. By incorporating the `payloadSchema` attribute into various components, we ensure that each task's payload structure is clearly defined and can be validated before processing.

- Updated the TaskManifest and task metadata structures to include an optional `payloadSchema` attribute. This addition allows for more robust validation and handling of task payloads.
- Enhanced several core modules to export and utilize the new `getSchemaToJsonSchema` function, providing easier conversion of schema types to JSON schemas.
- Modified the database schema to store the `payloadSchema` attribute, ensuring that the payload schema information is persisted.
- The change helps in maintaining consistency in data handling and improves the integrity of task data across the application.
2025-07-31 15:53:52 +01:00
71 changed files with 5638 additions and 500 deletions
+2 -1
View File
@@ -63,4 +63,5 @@ apps/**/public/build
/packages/core/src/package.json
/packages/trigger-sdk/src/package.json
/packages/python/src/package.json
.claude
.claude
.mcp.log
+1 -1
View File
@@ -19,7 +19,7 @@
"prom-client": "^15.1.0",
"socket.io": "4.7.4",
"std-env": "^3.8.0",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@types/dockerode": "^3.3.33"
@@ -77,7 +77,7 @@ export class CreateBackgroundWorkerService extends BaseService {
version: nextVersion,
runtimeEnvironmentId: environment.id,
projectId: project.id,
metadata: body.metadata,
metadata: body.metadata as any,
contentHash: body.metadata.contentHash,
cliVersion: body.metadata.cliPackageVersion,
sdkVersion: body.metadata.packageVersion,
@@ -280,6 +280,7 @@ async function createWorkerTask(
fileId: tasksToBackgroundFiles?.get(task.id) ?? null,
maxDurationInSeconds: task.maxDuration ? clampMaxDuration(task.maxDuration) : null,
queueId: queue.id,
payloadSchema: task.payloadSchema as any,
},
});
} catch (error) {
@@ -48,7 +48,7 @@ export class CreateDeploymentBackgroundWorkerServiceV3 extends BaseService {
version: deployment.version,
runtimeEnvironmentId: environment.id,
projectId: environment.projectId,
metadata: body.metadata,
metadata: body.metadata as any,
contentHash: body.metadata.contentHash,
cliVersion: body.metadata.cliPackageVersion,
sdkVersion: body.metadata.packageVersion,
@@ -65,7 +65,7 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
version: deployment.version,
runtimeEnvironmentId: environment.id,
projectId: environment.projectId,
metadata: body.metadata,
metadata: body.metadata as any,
contentHash: body.metadata.contentHash,
cliVersion: body.metadata.cliPackageVersion,
sdkVersion: body.metadata.packageVersion,
+1 -1
View File
@@ -203,7 +203,7 @@
"ulidx": "^2.2.1",
"uuid": "^9.0.0",
"ws": "^8.11.0",
"zod": "3.23.8",
"zod": "3.25.76",
"zod-error": "1.5.0",
"zod-validation-error": "^1.5.0"
},
+1 -1
View File
@@ -9,7 +9,7 @@
"@clickhouse/client": "^1.11.1",
"@internal/tracing": "workspace:*",
"@trigger.dev/core": "workspace:*",
"zod": "3.23.8",
"zod": "3.25.76",
"zod-error": "1.5.0"
},
"devDependencies": {
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "BackgroundWorkerTask" ADD COLUMN "payloadSchema" JSONB;
@@ -510,6 +510,8 @@ model BackgroundWorkerTask {
triggerSource TaskTriggerSource @default(STANDARD)
payloadSchema Json?
@@unique([workerId, slug])
// Quick lookup of task identifiers
@@index([projectId, slug])
+1 -1
View File
@@ -17,7 +17,7 @@
"react-email": "^2.1.1",
"resend": "^3.2.0",
"tiny-invariant": "^1.2.0",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@types/nodemailer": "^6.4.17",
+1 -1
View File
@@ -30,7 +30,7 @@
"nanoid": "3.3.8",
"redlock": "5.0.0-beta.2",
"seedrandom": "^3.0.5",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@internal/testcontainers": "workspace:*",
@@ -22,7 +22,7 @@
"cron-parser": "^4.9.0",
"cronstrue": "^2.50.0",
"nanoid": "3.3.8",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@internal/testcontainers": "workspace:*",
+1 -1
View File
@@ -10,7 +10,7 @@
"@trigger.dev/database": "workspace:*",
"graphile-worker": "0.16.6",
"lodash.omit": "^4.5.0",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@types/lodash.omit": "^4.5.7",
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
set -e # Exit on error
echo "🚀 Installing Trigger.dev MCP Server..."
# Get the absolute path to the node binary
NODE_PATH=$(which node)
if [ -z "$NODE_PATH" ]; then
echo "❌ Error: Node.js not found in PATH"
echo "Please ensure Node.js is installed and available in your PATH"
exit 1
fi
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Construct the path to the CLI index.js file
CLI_PATH="$SCRIPT_DIR/dist/esm/index.js"
# Construct the path to the MCP log file
MCP_LOG_FILE="$SCRIPT_DIR/.mcp.log"
# Make sure the MCP log file exists
touch "$MCP_LOG_FILE"
# Check if the CLI file exists
if [ ! -f "$CLI_PATH" ]; then
echo "❌ Error: CLI file not found at $CLI_PATH"
echo "Make sure to build the CLI first with: pnpm run build"
exit 1
fi
# Ensure the CLI is executable
chmod +x "$CLI_PATH"
echo "✅ Found Node.js at: $NODE_PATH"
echo "✅ Found CLI at: $CLI_PATH"
# Claude Code configuration
CLAUDE_CONFIG="$HOME/.claude.json"
echo "📁 Claude configuration file: $CLAUDE_CONFIG"
# Check if Claude config exists, create if it doesn't
if [ ! -f "$CLAUDE_CONFIG" ]; then
echo "📝 Creating new Claude configuration file..."
echo '{"mcpServers": {}}' > "$CLAUDE_CONFIG"
fi
# Use Node.js to manipulate the JSON
echo "🔧 Updating Claude configuration..."
node -e "
const fs = require('fs');
const path = require('path');
const configPath = '$CLAUDE_CONFIG';
const nodePath = '$NODE_PATH';
const cliPath = '$CLI_PATH';
const logFile = '$MCP_LOG_FILE';
try {
// Read existing config
let config;
try {
const configContent = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configContent);
} catch (error) {
console.log('📝 Creating new configuration structure...');
config = {};
}
// Ensure mcpServers object exists
if (!config.mcpServers) {
config.mcpServers = {};
}
// Add/update trigger.dev entry
config.mcpServers['trigger'] = {
command: nodePath,
args: [cliPath, 'mcp', '--log-file', logFile]
};
// Write back to file with proper formatting
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log('✅ Successfully installed Trigger.dev MCP server to Claude Code');
console.log('');
console.log('📋 Configuration Details:');
console.log(' • Config file:', configPath);
console.log(' • Node.js path:', nodePath);
console.log(' • CLI path:', cliPath);
console.log('');
console.log('🎉 Installation complete! You can now use Trigger.dev MCP commands in Claude Code.');
console.log('💡 Try typing @ in Claude Code and select \"triggerdev\" to get started.');
} catch (error) {
console.error('❌ Error updating Claude configuration:', error.message);
process.exit(1);
}
"
echo ""
echo "🔍 You can test the MCP server with:"
echo " pnpm run inspector"
+6 -3
View File
@@ -75,12 +75,14 @@
"dev": "tshy --watch",
"test": "vitest",
"test:e2e": "vitest --run -c ./e2e/vitest.config.ts",
"update-version": "tsx ../../scripts/updateVersion.ts"
"update-version": "tsx ../../scripts/updateVersion.ts",
"install-mcp": "./install-mcp.sh",
"inspector": "npx @modelcontextprotocol/inspector dist/esm/index.js mcp --log-file .mcp.log"
},
"dependencies": {
"@clack/prompts": "^0.10.0",
"@depot/cli": "0.0.1-cli.2.80.0",
"@modelcontextprotocol/sdk": "^1.6.1",
"@modelcontextprotocol/sdk": "^1.17.0",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/api-logs": "0.52.1",
"@opentelemetry/exporter-logs-otlp-http": "0.52.1",
@@ -95,6 +97,7 @@
"@opentelemetry/semantic-conventions": "1.25.1",
"@trigger.dev/build": "workspace:4.0.0-v4-beta.25",
"@trigger.dev/core": "workspace:4.0.0-v4-beta.25",
"@trigger.dev/schema-to-json": "workspace:4.0.0-v4-beta.25",
"ansi-escapes": "^7.0.0",
"braces": "^3.0.3",
"c12": "^1.11.1",
@@ -138,7 +141,7 @@
"tinyglobby": "^0.2.10",
"ws": "^8.18.0",
"xdg-app-paths": "^8.3.0",
"zod": "3.23.8",
"zod": "3.25.76",
"zod-validation-error": "^1.5.0"
},
"engines": {
+12 -12
View File
@@ -1,21 +1,20 @@
import { Command } from "commander";
import { configureAnalyzeCommand } from "../commands/analyze.js";
import { configureDeployCommand } from "../commands/deploy.js";
import { configureDevCommand } from "../commands/dev.js";
import { configureInitCommand } from "../commands/init.js";
import { configureListProfilesCommand } from "../commands/list-profiles.js";
import { configureLoginCommand } from "../commands/login.js";
import { configureLogoutCommand } from "../commands/logout.js";
import { configureWhoamiCommand } from "../commands/whoami.js";
import { COMMAND_NAME } from "../consts.js";
import { configureListProfilesCommand } from "../commands/list-profiles.js";
import { configureAnalyzeCommand } from "../commands/analyze.js";
import { configureUpdateCommand } from "../commands/update.js";
import { VERSION } from "../version.js";
import { configureDeployCommand } from "../commands/deploy.js";
import { installExitHandler } from "./common.js";
import { configureWorkersCommand } from "../commands/workers/index.js";
import { configureSwitchProfilesCommand } from "../commands/switch.js";
import { configureTriggerTaskCommand } from "../commands/trigger.js";
import { configurePromoteCommand } from "../commands/promote.js";
import { configurePreviewCommand } from "../commands/preview.js";
import { configurePromoteCommand } from "../commands/promote.js";
import { configureSwitchProfilesCommand } from "../commands/switch.js";
import { configureUpdateCommand } from "../commands/update.js";
import { configureWhoamiCommand } from "../commands/whoami.js";
import { configureMcpCommand } from "../commands/mcp.js";
import { COMMAND_NAME } from "../consts.js";
import { VERSION } from "../version.js";
import { installExitHandler } from "./common.js";
export const program = new Command();
@@ -24,6 +23,7 @@ program
.description("Create, run locally and deploy Trigger.dev background tasks.")
.version(VERSION, "-v, --version", "Display the version number");
configureMcpCommand(program);
configureLoginCommand(program);
configureInitCommand(program);
configureDevCommand(program);
+85
View File
@@ -0,0 +1,85 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Command } from "commander";
import { z } from "zod";
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
import { login } from "./login.js";
import { performSearch } from "../mcp/mintlifyClient.js";
import { logger } from "../utilities/logger.js";
import { FileLogger } from "../mcp/logger.js";
import { McpContext } from "../mcp/context.js";
import { registerGetProjectDetailsTool } from "../mcp/tools.js";
const McpCommandOptions = CommonCommandOptions.extend({
projectRef: z.string().optional(),
logFile: z.string().optional(),
});
export type McpCommandOptions = z.infer<typeof McpCommandOptions>;
export function configureMcpCommand(program: Command) {
return commonOptions(
program
.command("mcp")
.description("Run the MCP server")
.option("-p, --project-ref <project ref>", "The project ref to use")
.option("--log-file <log file>", "The file to log to")
).action(async (options) => {
wrapCommandAction("mcp", McpCommandOptions, options, async (opts) => {
await mcpCommand(opts);
});
});
}
export async function mcpCommand(options: McpCommandOptions) {
logger.loggerLevel = "none";
const authorization = await login({
embedded: true,
silent: true,
defaultApiUrl: options.apiUrl,
profile: options.profile,
});
if (!authorization.ok) {
process.exitCode = 1;
return;
}
const server = new McpServer({
name: "triggerdev",
version: "1.0.0",
description: "Trigger.dev MCP server. Search the Trigger.dev docs.",
});
const fileLogger: FileLogger | undefined = options.logFile
? new FileLogger(options.logFile, server)
: undefined;
const context = new McpContext(server, {
login: authorization,
projectRef: options.projectRef,
fileLogger,
});
server.registerTool(
"search_docs",
{
description:
"Search across the Trigger.dev documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about Trigger.dev, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages",
inputSchema: {
query: z.string(),
},
},
async ({ query }) => {
const results = await performSearch(query);
return results;
}
);
registerGetProjectDetailsTool(context);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
}
+1 -1
View File
@@ -27,7 +27,7 @@ const server = new McpServer({
// This could be a good fit for the `resource` entity in MCP.
// Also, a custom `prompt` entity could be useful to instruct the LLM to prompt the user
// for selecting a task from a list of matching tasks, when the confidence for an exact match is low.
server.tool("list-all-tasks", "List all available task IDs in the worker.", async () => {
server.tool("list-all-tasks", "List all available task IDs in the worker.", async (params) => {
return {
content: [
{
@@ -18,6 +18,7 @@ import { registerResources } from "../indexing/registerResources.js";
import { env } from "std-env";
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
import { schemaToJsonSchema, initializeSchemaConverters } from "@trigger.dev/schema-to-json";
sourceMapSupport.install({
handleUncaughtExceptions: false,
@@ -100,7 +101,7 @@ async function bootstrap() {
const { buildManifest, importErrors, config, timings } = await bootstrap();
let tasks = resourceCatalog.listTaskManifests();
let tasks = await convertSchemasToJsonSchemas(resourceCatalog.listTaskManifests());
// If the config has retry defaults, we need to apply them to all tasks that don't have any retry settings
if (config.retries?.default) {
@@ -190,3 +191,20 @@ await new Promise<void>((resolve) => {
resolve();
}, 10);
});
async function convertSchemasToJsonSchemas(tasks: TaskManifest[]): Promise<TaskManifest[]> {
await initializeSchemaConverters();
const convertedTasks = tasks.map((task) => {
const schema = resourceCatalog.getTaskSchema(task.id);
if (schema) {
const result = schemaToJsonSchema(schema);
return { ...task, payloadSchema: result?.jsonSchema };
}
return task;
});
return convertedTasks;
}
@@ -18,6 +18,7 @@ import { registerResources } from "../indexing/registerResources.js";
import { env } from "std-env";
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
import { schemaToJsonSchema, initializeSchemaConverters } from "@trigger.dev/schema-to-json";
sourceMapSupport.install({
handleUncaughtExceptions: false,
@@ -100,7 +101,7 @@ async function bootstrap() {
const { buildManifest, importErrors, config, timings } = await bootstrap();
let tasks = resourceCatalog.listTaskManifests();
let tasks = await convertSchemasToJsonSchemas(resourceCatalog.listTaskManifests());
// If the config has retry defaults, we need to apply them to all tasks that don't have any retry settings
if (config.retries?.default) {
@@ -196,3 +197,20 @@ await new Promise<void>((resolve) => {
resolve();
}, 10);
});
async function convertSchemasToJsonSchemas(tasks: TaskManifest[]): Promise<TaskManifest[]> {
await initializeSchemaConverters();
const convertedTasks = tasks.map((task) => {
const schema = resourceCatalog.getTaskSchema(task.id);
if (schema) {
const result = schemaToJsonSchema(schema);
return { ...task, payloadSchema: result?.jsonSchema };
}
return task;
});
return convertedTasks;
}
+23
View File
@@ -0,0 +1,23 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { FileLogger } from "./logger.js";
import { LoginResult } from "../utilities/session.js";
export type McpContextOptions = {
login: LoginResult;
projectRef?: string;
fileLogger?: FileLogger;
};
export class McpContext {
public readonly server: McpServer;
public readonly options: McpContextOptions;
constructor(server: McpServer, options: McpContextOptions) {
this.server = server;
this.options = options;
}
get logger() {
return this.options.fileLogger;
}
}
+47
View File
@@ -0,0 +1,47 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { appendFileSync } from "node:fs";
import util from "node:util";
export class FileLogger {
private filePath: string;
private server: McpServer;
constructor(filePath: string, server: McpServer) {
this.filePath = filePath;
this.server = server;
}
log(message: string, ...args: unknown[]) {
const logMessage = `[${new Date().toISOString()}][${this.formatServerInfo()}] ${message} - ${util.inspect(
args,
{
depth: null,
colors: false,
}
)}\n`;
appendFileSync(this.filePath, logMessage);
}
private formatServerInfo() {
return `${this.formatClientName()} ${this.formatClientVersion()} ${this.formatClientCapabilities()}`;
}
private formatClientName() {
const clientName = this.server.server.getClientVersion()?.name;
return `client=${clientName ?? "unknown"}`;
}
private formatClientVersion() {
const clientVersion = this.server.server.getClientVersion();
return `version=${clientVersion?.version ?? "unknown"}`;
}
private formatClientCapabilities() {
const clientCapabilities = this.server.server.getClientCapabilities();
const keys = Object.keys(clientCapabilities ?? {});
return `capabilities=${keys.join(",")}`;
}
}
+73
View File
@@ -0,0 +1,73 @@
export async function performSearch(query: string) {
const body = callToolBody("search", { query });
const response = await fetch("https://trigger.dev/docs/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-06-18",
},
body: JSON.stringify(body),
});
const data = await parseResponse(response);
return data;
}
async function parseResponse(response: Response) {
if (response.headers.get("content-type")?.includes("text/event-stream")) {
return parseSSEResponse(response);
} else {
return parseJSONResponse(response);
}
}
async function parseJSONResponse(response: Response) {
const data = await response.json();
return data;
}
// Get the first data: event and return the parsed JSON of the event
async function parseSSEResponse(response: Response) {
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error("No reader found");
}
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) throw new Error("SSE stream closed before data arrived");
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n"); // SSE delimiter
buffer = events.pop()!; // keep incomplete
for (const evt of events) {
for (const line of evt.split("\n")) {
if (line.startsWith("data:")) {
const json = line.slice(5).trim();
return JSON.parse(json); // ✅ got it
}
}
}
}
throw new Error("No data: event found");
}
function callToolBody(tool: string, args: Record<string, unknown>) {
return {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: tool,
arguments: args,
},
};
}
+23
View File
@@ -0,0 +1,23 @@
import z from "zod";
import { McpContext } from "./context.js";
export function registerGetProjectDetailsTool(context: McpContext) {
context.server.registerTool(
"get_project_details",
{
description: "Get the details of the project",
inputSchema: {
projectRef: z.string().optional(),
},
},
async ({ projectRef }, extra) => {
const roots = await context.server.server.listRoots();
context.logger?.log("get_project_details", { roots, projectRef, extra });
return {
content: [{ type: "text", text: "Not implemented" }],
};
}
);
}
+1 -1
View File
@@ -197,7 +197,7 @@
"superjson": "^2.2.1",
"tinyexec": "^0.3.2",
"uncrypto": "^0.1.3",
"zod": "3.23.8",
"zod": "3.25.76",
"zod-error": "1.5.0",
"zod-validation-error": "^1.5.0"
},
@@ -1,5 +1,5 @@
import { QueueManifest, TaskManifest, WorkerManifest } from "../schemas/index.js";
import { TaskMetadataWithFunctions } from "../types/index.js";
import { TaskMetadataWithFunctions, TaskSchema } from "../types/index.js";
export interface ResourceCatalog {
setCurrentFileContext(filePath: string, entryPoint: string): void;
@@ -13,4 +13,5 @@ export interface ResourceCatalog {
registerWorkerManifest(workerManifest: WorkerManifest): void;
registerQueueMetadata(queue: QueueManifest): void;
listQueueManifests(): Array<QueueManifest>;
getTaskSchema(id: string): TaskSchema | undefined;
}
@@ -1,7 +1,7 @@
const API_NAME = "resource-catalog";
import { QueueManifest, TaskManifest, WorkerManifest } from "../schemas/index.js";
import { TaskMetadataWithFunctions } from "../types/index.js";
import { TaskMetadataWithFunctions, TaskSchema } from "../types/index.js";
import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js";
import { type ResourceCatalog } from "./catalog.js";
import { NoopResourceCatalog } from "./noopResourceCatalog.js";
@@ -65,6 +65,10 @@ export class ResourceCatalogAPI {
return this.#getCatalog().getTask(id);
}
public getTaskSchema(id: string): TaskSchema | undefined {
return this.#getCatalog().getTaskSchema(id);
}
public taskExists(id: string): boolean {
return this.#getCatalog().taskExists(id);
}
@@ -1,5 +1,5 @@
import { QueueManifest, TaskManifest, WorkerManifest } from "../schemas/index.js";
import { TaskMetadataWithFunctions } from "../types/index.js";
import { TaskMetadataWithFunctions, TaskSchema } from "../types/index.js";
import { ResourceCatalog } from "./catalog.js";
export class NoopResourceCatalog implements ResourceCatalog {
@@ -31,6 +31,10 @@ export class NoopResourceCatalog implements ResourceCatalog {
return undefined;
}
getTaskSchema(id: string): TaskSchema | undefined {
return undefined;
}
taskExists(id: string): boolean {
return false;
}
@@ -5,10 +5,11 @@ import {
WorkerManifest,
QueueManifest,
} from "../schemas/index.js";
import { TaskMetadataWithFunctions } from "../types/index.js";
import { TaskMetadataWithFunctions, TaskSchema } from "../types/index.js";
import { ResourceCatalog } from "./catalog.js";
export class StandardResourceCatalog implements ResourceCatalog {
private _taskSchemas: Map<string, TaskSchema> = new Map();
private _taskMetadata: Map<string, TaskMetadata> = new Map();
private _taskFunctions: Map<string, TaskMetadataWithFunctions["fns"]> = new Map();
private _taskFileMetadata: Map<string, TaskFileMetadata> = new Map();
@@ -72,6 +73,10 @@ export class StandardResourceCatalog implements ResourceCatalog {
this._taskMetadata.set(task.id, metadata);
this._taskFunctions.set(task.id, fns);
if (task.schema) {
this._taskSchemas.set(task.id, task.schema);
}
}
updateTaskMetadata(id: string, updates: Partial<TaskMetadataWithFunctions>): void {
@@ -107,15 +112,21 @@ export class StandardResourceCatalog implements ResourceCatalog {
continue;
}
result.push({
const taskManifest = {
...metadata,
...fileMetadata,
});
};
result.push(taskManifest);
}
return result;
}
getTaskSchema(id: string): TaskSchema | undefined {
return this._taskSchemas.get(id);
}
listQueueManifests(): Array<QueueManifest> {
return Array.from(this._queueMetadata.values());
}
@@ -13,6 +13,8 @@ export const TaskResource = z.object({
triggerSource: z.string().optional(),
schedule: ScheduleMetadata.optional(),
maxDuration: z.number().optional(),
// JSONSchema type - using z.unknown() for runtime validation to accept JSONSchema7
payloadSchema: z.unknown().optional(),
});
export type TaskResource = z.infer<typeof TaskResource>;
+1
View File
@@ -189,6 +189,7 @@ const taskMetadata = {
triggerSource: z.string().optional(),
schedule: ScheduleMetadata.optional(),
maxDuration: z.number().optional(),
payloadSchema: z.unknown().optional(),
};
export const TaskMetadata = z.object(taskMetadata);
+1
View File
@@ -7,6 +7,7 @@ export * from "./tasks.js";
export * from "./idempotencyKeys.js";
export * from "./tools.js";
export * from "./queues.js";
export * from "./jsonSchema.js";
type ResolveEnvironmentVariablesOptions = {
variables: Record<string, string> | Array<{ name: string; value: string }>;
+76
View File
@@ -0,0 +1,76 @@
/**
* JSON Schema type definition - compatible with JSON Schema Draft 7
* Based on the JSONSchema7 type from @types/json-schema but defined inline to avoid import issues
*/
export interface JSONSchema {
$id?: string;
$ref?: string;
$schema?: string;
$comment?: string;
type?: JSONSchemaType | JSONSchemaType[];
enum?: any[];
const?: any;
// Number/Integer validations
multipleOf?: number;
maximum?: number;
exclusiveMaximum?: number;
minimum?: number;
exclusiveMinimum?: number;
// String validations
maxLength?: number;
minLength?: number;
pattern?: string;
format?: string;
// Array validations
items?: JSONSchema | JSONSchema[];
additionalItems?: JSONSchema | boolean;
maxItems?: number;
minItems?: number;
uniqueItems?: boolean;
contains?: JSONSchema;
// Object validations
maxProperties?: number;
minProperties?: number;
required?: string[];
properties?: Record<string, JSONSchema>;
patternProperties?: Record<string, JSONSchema>;
additionalProperties?: JSONSchema | boolean;
dependencies?: Record<string, JSONSchema | string[]>;
propertyNames?: JSONSchema;
// Conditional schemas
if?: JSONSchema;
then?: JSONSchema;
else?: JSONSchema;
// Boolean logic
allOf?: JSONSchema[];
anyOf?: JSONSchema[];
oneOf?: JSONSchema[];
not?: JSONSchema;
// Metadata
title?: string;
description?: string;
default?: any;
readOnly?: boolean;
writeOnly?: boolean;
examples?: any[];
// Additional properties for extensibility
[key: string]: any;
}
export type JSONSchemaType =
| "string"
| "number"
| "integer"
| "boolean"
| "object"
| "array"
| "null";
+17
View File
@@ -28,6 +28,7 @@ import { QueueOptions } from "./queues.js";
import { AnySchemaParseFn, inferSchemaIn, inferSchemaOut, Schema } from "./schemas.js";
import { inferToolParameters, ToolTaskParameters } from "./tools.js";
import { Prettify } from "./utils.js";
import { JSONSchema } from "./jsonSchema.js";
export type Queue = QueueOptions;
export type TaskSchema = Schema;
@@ -339,6 +340,12 @@ type CommonTaskOptions<
* onFailure is called after a task run has failed (meaning the run function threw an error and won't be retried anymore)
*/
onFailure?: OnFailureHookFunction<TPayload, TInitOutput>;
/**
* JSON Schema for the task payload. This will be synced to the server during indexing.
* Should be a valid JSON Schema Draft 7 object.
*/
jsonSchema?: JSONSchema;
};
export type TaskOptions<
@@ -348,6 +355,15 @@ export type TaskOptions<
TInitOutput extends InitOutput = any,
> = CommonTaskOptions<TIdentifier, TPayload, TOutput, TInitOutput>;
// Task options when payloadSchema is provided - payload should be any
export type TaskOptionsWithSchema<
TIdentifier extends string,
TOutput = unknown,
TInitOutput extends InitOutput = any,
> = CommonTaskOptions<TIdentifier, any, TOutput, TInitOutput> & {
jsonSchema: JSONSchema;
};
export type TaskWithSchemaOptions<
TIdentifier extends string,
TSchema extends TaskSchema | undefined = undefined,
@@ -881,6 +897,7 @@ export type TaskMetadataWithFunctions = TaskMetadata & {
onStart?: (payload: any, params: StartFnParams) => Promise<void>;
parsePayload?: AnySchemaParseFn;
};
schema?: TaskSchema;
};
export type RunTypes<TTaskIdentifier extends string, TPayload, TOutput> = {
+1 -1
View File
@@ -27,7 +27,7 @@
"lodash.omit": "^4.5.0",
"nanoid": "^5.0.7",
"p-limit": "^6.2.0",
"zod": "3.23.8",
"zod": "3.25.76",
"cron-parser": "^4.9.0"
},
"devDependencies": {
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
.tshy
.tshy-build
*.log
.DS_Store
+151
View File
@@ -0,0 +1,151 @@
# @trigger.dev/schema-to-json
Convert various schema validation libraries to JSON Schema format.
## Installation
```bash
npm install @trigger.dev/schema-to-json
```
## Important: Bundle Safety
This package is designed to be **bundle-safe**. It does NOT bundle any schema libraries (zod, yup, etc.) as dependencies. Instead:
1. **Built-in conversions** work immediately (ArkType, Zod 4, TypeBox)
2. **External conversions** (Zod 3, Yup, Effect) require the conversion libraries to be available at runtime
This design ensures that:
- ✅ Your bundle size stays small
- ✅ You only include the schema libraries you actually use
- ✅ Tree-shaking works properly
- ✅ No unnecessary dependencies are installed
## Supported Schema Libraries
-**Zod** - Full support
- Zod 4: Native support via built-in `toJsonSchema` method (no external deps needed)
- Zod 3: Requires `zod-to-json-schema` to be installed
-**Yup** - Requires `@sodaru/yup-to-json-schema` to be installed
-**ArkType** - Native support (built-in `toJsonSchema` method)
-**Effect/Schema** - Requires `effect` or `@effect/schema` to be installed
-**TypeBox** - Native support (already JSON Schema compliant)
-**Valibot** - Coming soon
-**Superstruct** - Coming soon
-**Runtypes** - Coming soon
## Usage
### Basic Usage (Built-in conversions only)
```typescript
import { schemaToJsonSchema } from '@trigger.dev/schema-to-json';
import { type } from 'arktype';
// Works immediately for schemas with built-in conversion
const arkSchema = type({
name: 'string',
age: 'number',
});
const result = schemaToJsonSchema(arkSchema);
console.log(result);
// { jsonSchema: {...}, schemaType: 'arktype' }
```
### Full Usage (With external conversion libraries)
```typescript
import { schemaToJsonSchema, initializeSchemaConverters } from '@trigger.dev/schema-to-json';
import { z } from 'zod';
// Initialize converters once in your app (loads conversion libraries if available)
await initializeSchemaConverters();
// Now you can convert Zod 3, Yup, and Effect schemas
const zodSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
const result = schemaToJsonSchema(zodSchema);
console.log(result);
// {
// jsonSchema: {
// type: 'object',
// properties: {
// name: { type: 'string' },
// age: { type: 'number' },
// email: { type: 'string', format: 'email' }
// },
// required: ['name', 'age', 'email']
// },
// schemaType: 'zod'
// }
```
## API
### `schemaToJsonSchema(schema, options?)`
Convert a schema to JSON Schema format.
**Parameters:**
- `schema` - The schema to convert
- `options` (optional)
- `name` - Name to use for the schema (supported by some converters)
- `additionalProperties` - Additional properties to merge into the result
**Returns:**
- `{ jsonSchema, schemaType }` - The converted JSON Schema and detected type
- `undefined` - If the schema cannot be converted
### `initializeSchemaConverters()`
Initialize the external conversion libraries. Call this once in your application if you need to convert schemas that don't have built-in JSON Schema support (Zod 3, Yup, Effect).
**Returns:** `Promise<void>`
### `canConvertSchema(schema)`
Check if a schema can be converted to JSON Schema.
**Returns:** `boolean`
### `detectSchemaType(schema)`
Detect the type of schema.
**Returns:** `'zod' | 'yup' | 'arktype' | 'effect' | 'valibot' | 'superstruct' | 'runtypes' | 'typebox' | 'unknown'`
### `areConvertersInitialized()`
Check which conversion libraries are available.
**Returns:** `{ zod: boolean, yup: boolean, effect: boolean }`
## Peer Dependencies
Each schema library is an optional peer dependency. Install only the ones you need:
```bash
# For Zod
npm install zod
# For Yup
npm install yup
# For ArkType
npm install arktype
# For Effect
npm install effect @effect/schema
# For TypeBox
npm install @sinclair/typebox
```
## License
MIT
+112
View File
@@ -0,0 +1,112 @@
{
"name": "@trigger.dev/schema-to-json",
"version": "4.0.0-v4-beta.25",
"description": "Convert various schema validation libraries to JSON Schema",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/triggerdotdev/trigger.dev",
"directory": "packages/schema-to-json"
},
"type": "module",
"engines": {
"node": ">=18.20.0"
},
"files": [
"dist"
],
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
}
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run build:tshy",
"build:tshy": "tshy",
"dev": "tshy --watch",
"typecheck": "tsc -p tsconfig.src.json --noEmit",
"test": "vitest",
"update-version": "tsx ../../scripts/updateVersion.ts",
"check-exports": "attw --pack ."
},
"dependencies": {
"@trigger.dev/core": "workspace:*",
"zod-to-json-schema": "^3.24.5",
"@sodaru/yup-to-json-schema": "^2.0.1"
},
"devDependencies": {
"@effect/schema": "^0.75.5",
"arktype": "^2.0.0",
"effect": "^3.11.11",
"runtypes": "^6.7.0",
"superstruct": "^2.0.2",
"tshy": "^3.0.2",
"@sinclair/typebox": "^0.34.3",
"valibot": "^1.0.0-beta.8",
"vitest": "^2.1.8",
"yup": "^1.6.1",
"zod": "^3.24.1 || ^4.0.0",
"rimraf": "6.0.1"
},
"peerDependencies": {
"@effect/schema": "^0.75.5",
"arktype": "^2.0.0",
"effect": "^3.11.11",
"runtypes": "^6.7.0",
"superstruct": "^2.0.2",
"@sinclair/typebox": "^0.34.3",
"valibot": "^1.0.0-beta.8",
"yup": "^1.6.1",
"zod": "^3.24.1 || ^4.0.0"
},
"peerDependenciesMeta": {
"@effect/schema": {
"optional": true
},
"arktype": {
"optional": true
},
"effect": {
"optional": true
},
"runtypes": {
"optional": true
},
"superstruct": {
"optional": true
},
"typebox": {
"optional": true
},
"valibot": {
"optional": true
},
"yup": {
"optional": true
},
"zod": {
"optional": true
}
},
"tshy": {
"selfLink": false,
"exports": {
".": "./src/index.ts"
},
"project": "./tsconfig.src.json"
},
"main": "./dist/commonjs/index.js",
"types": "./dist/commonjs/index.d.ts",
"module": "./dist/esm/index.js"
}
+243
View File
@@ -0,0 +1,243 @@
// Import JSONSchema from core to ensure compatibility
import type { JSONSchema } from "@trigger.dev/core/v3";
export type Schema = unknown;
export type { JSONSchema };
export interface ConversionOptions {
/**
* The name to use for the schema in the JSON Schema
*/
name?: string;
/**
* Additional JSON Schema properties to merge
*/
additionalProperties?: Record<string, unknown>;
}
export interface ConversionResult {
/**
* The JSON Schema representation (JSON Schema Draft 7)
*/
jsonSchema: JSONSchema;
/**
* The detected schema type
*/
schemaType:
| "zod"
| "yup"
| "arktype"
| "effect"
| "valibot"
| "superstruct"
| "runtypes"
| "typebox"
| "unknown";
}
/**
* Convert a schema from various validation libraries to JSON Schema
*
* This function attempts to convert schemas without requiring external dependencies to be bundled.
* It will only succeed if:
* 1. The schema has built-in JSON Schema conversion (ArkType, Zod 4, TypeBox)
* 2. The required conversion library is available at runtime (zod-to-json-schema, @sodaru/yup-to-json-schema, etc.)
*
* @param schema The schema to convert
* @param options Optional conversion options
* @returns The conversion result or undefined if conversion is not possible
*/
export function schemaToJsonSchema(
schema: Schema,
options?: ConversionOptions
): ConversionResult | undefined {
const parser = schema as any;
// Check if schema has a built-in toJsonSchema method (e.g., ArkType, Zod 4)
if (typeof parser.toJsonSchema === "function") {
try {
const jsonSchema = parser.toJsonSchema();
// Determine if it's Zod or ArkType based on other methods
const schemaType =
typeof parser.parseAsync === "function" || typeof parser.parse === "function"
? "zod"
: "arktype";
return {
jsonSchema: options?.additionalProperties
? { ...jsonSchema, ...options.additionalProperties }
: jsonSchema,
schemaType,
};
} catch (error) {
// If toJsonSchema fails, continue to other checks
}
}
// Check if it's a TypeBox schema (has Static and Kind symbols)
if (parser[Symbol.for("TypeBox.Kind")] !== undefined) {
// TypeBox schemas are already JSON Schema compliant
return {
jsonSchema: options?.additionalProperties
? { ...parser, ...options.additionalProperties }
: parser,
schemaType: "typebox",
};
}
// For schemas that need external libraries, we need to check if they're available
// This approach avoids bundling the dependencies while still allowing runtime usage
// Check if it's a Zod schema (without built-in toJsonSchema)
if (typeof parser.parseAsync === "function" || typeof parser.parse === "function") {
try {
// Try to access zod-to-json-schema if it's available
// @ts-ignore - This is intentionally dynamic
if (typeof globalThis.__zodToJsonSchema !== "undefined") {
// @ts-ignore
const { zodToJsonSchema } = globalThis.__zodToJsonSchema;
const jsonSchema = options?.name
? zodToJsonSchema(parser, options.name)
: zodToJsonSchema(parser);
if (jsonSchema && typeof jsonSchema === "object" && "$schema" in jsonSchema) {
const { $schema, ...rest } = jsonSchema as any;
return {
jsonSchema: options?.additionalProperties
? { ...rest, ...options.additionalProperties }
: rest,
schemaType: "zod",
};
}
return {
jsonSchema: options?.additionalProperties
? { ...jsonSchema, ...options.additionalProperties }
: jsonSchema,
schemaType: "zod",
};
}
} catch (error) {
// Library not available
}
}
// Check if it's a Yup schema
if (typeof parser.validateSync === "function" && typeof parser.describe === "function") {
try {
// @ts-ignore
if (typeof globalThis.__yupToJsonSchema !== "undefined") {
// @ts-ignore
const { convertSchema } = globalThis.__yupToJsonSchema;
const jsonSchema = convertSchema(parser);
return {
jsonSchema: options?.additionalProperties
? { ...jsonSchema, ...options.additionalProperties }
: jsonSchema,
schemaType: "yup",
};
}
} catch (error) {
// Library not available
}
}
// Check if it's an Effect schema
if (
parser._tag === "Schema" ||
parser._tag === "SchemaClass" ||
typeof parser.ast === "function"
) {
try {
// @ts-ignore
if (typeof globalThis.__effectJsonSchema !== "undefined") {
// @ts-ignore
const { JSONSchema } = globalThis.__effectJsonSchema;
const jsonSchema = JSONSchema.make(parser);
return {
jsonSchema: options?.additionalProperties
? { ...jsonSchema, ...options.additionalProperties }
: jsonSchema,
schemaType: "effect",
};
}
} catch (error) {
// Library not available
}
}
// Future schema types can be added here...
// Unknown schema type
return undefined;
}
/**
* Initialize the schema conversion libraries
* This should be called by the consuming application if they want to enable
* conversion for schemas that don't have built-in JSON Schema support
*/
export async function initializeSchemaConverters(): Promise<void> {
try {
// @ts-ignore
globalThis.__zodToJsonSchema = await import("zod-to-json-schema");
} catch {
// Zod conversion not available
}
try {
// @ts-ignore
globalThis.__yupToJsonSchema = await import("@sodaru/yup-to-json-schema");
} catch {
// Yup conversion not available
}
try {
// Try Effect first, then @effect/schema
let module;
try {
module = await import("effect");
} catch {
module = await import("@effect/schema");
}
if (module?.JSONSchema) {
// @ts-ignore
globalThis.__effectJsonSchema = { JSONSchema: module.JSONSchema };
}
} catch {
// Effect conversion not available
}
}
/**
* Check if a schema can be converted to JSON Schema
*/
export function canConvertSchema(schema: Schema): boolean {
const result = schemaToJsonSchema(schema);
return result !== undefined;
}
/**
* Get the detected schema type
*/
export function detectSchemaType(schema: Schema): ConversionResult["schemaType"] {
const result = schemaToJsonSchema(schema);
return result?.schemaType ?? "unknown";
}
/**
* Check if the conversion libraries are initialized
*/
export function areConvertersInitialized(): {
zod: boolean;
yup: boolean;
effect: boolean;
} {
return {
// @ts-ignore
zod: typeof globalThis.__zodToJsonSchema !== "undefined",
// @ts-ignore
yup: typeof globalThis.__yupToJsonSchema !== "undefined",
// @ts-ignore
effect: typeof globalThis.__effectJsonSchema !== "undefined",
};
}
@@ -0,0 +1,351 @@
import { describe, it, expect, beforeAll } from "vitest";
import { z } from "zod";
import * as y from "yup";
// @ts-ignore
import { type } from "arktype";
import { Schema } from "@effect/schema";
import { Type } from "@sinclair/typebox";
import {
schemaToJsonSchema,
canConvertSchema,
detectSchemaType,
initializeSchemaConverters,
areConvertersInitialized,
} from "../index.js";
// Initialize converters before running tests
beforeAll(async () => {
await initializeSchemaConverters();
});
describe("schemaToJsonSchema", () => {
describe("Initialization", () => {
it("should have converters initialized", () => {
const status = areConvertersInitialized();
expect(status.zod).toBe(true);
expect(status.yup).toBe(true);
expect(status.effect).toBe(true);
});
});
describe("Zod schemas", () => {
it("should convert a simple Zod object schema", () => {
const schema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.schemaType).toBe("zod");
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
email: { type: "string", format: "email" },
},
required: ["name", "age", "email"],
});
});
it("should convert a Zod schema with optional fields", () => {
const schema = z.object({
id: z.string(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
id: { type: "string" },
description: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["id"],
});
});
it("should handle Zod schema with name option", () => {
const schema = z.object({
value: z.number(),
});
const result = schemaToJsonSchema(schema, { name: "MySchema" });
expect(result).toBeDefined();
expect(result?.jsonSchema).toBeDefined();
// The exact structure depends on zod-to-json-schema implementation
});
it("should handle Zod 4 schema with built-in toJsonSchema method", () => {
// Mock a Zod 4 schema with toJsonSchema method
const mockZod4Schema = {
parse: (val: unknown) => val,
parseAsync: async (val: unknown) => val,
toJsonSchema: () => ({
type: "object",
properties: {
id: { type: "string" },
count: { type: "number" },
},
required: ["id", "count"],
}),
};
const result = schemaToJsonSchema(mockZod4Schema);
expect(result).toBeDefined();
expect(result?.schemaType).toBe("zod");
expect(result?.jsonSchema).toEqual({
type: "object",
properties: {
id: { type: "string" },
count: { type: "number" },
},
required: ["id", "count"],
});
});
});
describe("Yup schemas", () => {
it("should convert a simple Yup object schema", () => {
const schema = y.object({
name: y.string().required(),
age: y.number().required(),
email: y.string().email().required(),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.schemaType).toBe("yup");
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
email: { type: "string", format: "email" },
},
required: ["name", "age", "email"],
});
});
it("should convert a Yup schema with optional fields", () => {
const schema = y.object({
id: y.string().required(),
description: y.string(),
count: y.number().min(0).max(100),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
id: { type: "string" },
description: { type: "string" },
count: { type: "number", minimum: 0, maximum: 100 },
},
required: ["id"],
});
});
});
describe("ArkType schemas", () => {
it("should convert a simple ArkType schema", () => {
const schema = type({
name: "string",
age: "number",
active: "boolean",
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.schemaType).toBe("arktype");
expect(result?.jsonSchema).toBeDefined();
expect(result?.jsonSchema.type).toBe("object");
});
it("should convert an ArkType schema with optional fields", () => {
const schema = type({
id: "string",
"description?": "string",
"tags?": "string[]",
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.jsonSchema).toBeDefined();
expect(result?.jsonSchema.type).toBe("object");
});
});
describe("Effect schemas", () => {
it("should convert a simple Effect schema", () => {
const schema = Schema.Struct({
name: Schema.String,
age: Schema.Number,
active: Schema.Boolean,
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.schemaType).toBe("effect");
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
active: { type: "boolean" },
},
required: ["name", "age", "active"],
});
});
it("should convert an Effect schema with optional fields", () => {
const schema = Schema.Struct({
id: Schema.String,
description: Schema.optional(Schema.String),
count: Schema.optional(Schema.Number),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.jsonSchema).toBeDefined();
expect(result?.jsonSchema.type).toBe("object");
});
});
describe("TypeBox schemas", () => {
it("should convert a simple TypeBox schema", () => {
const schema = Type.Object({
name: Type.String(),
age: Type.Number(),
active: Type.Boolean(),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.schemaType).toBe("typebox");
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
active: { type: "boolean" },
},
required: ["name", "age", "active"],
});
});
it("should convert a TypeBox schema with optional fields", () => {
const schema = Type.Object({
id: Type.String(),
description: Type.Optional(Type.String()),
tags: Type.Optional(Type.Array(Type.String())),
});
const result = schemaToJsonSchema(schema);
expect(result).toBeDefined();
expect(result?.jsonSchema).toMatchObject({
type: "object",
properties: {
id: { type: "string" },
description: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["id"],
});
});
});
describe("Additional options", () => {
it("should merge additional properties", () => {
const schema = z.object({
value: z.number(),
});
const result = schemaToJsonSchema(schema, {
additionalProperties: {
title: "My Schema",
description: "A test schema",
"x-custom": "custom value",
},
});
expect(result).toBeDefined();
expect(result?.jsonSchema.title).toBe("My Schema");
expect(result?.jsonSchema.description).toBe("A test schema");
expect(result?.jsonSchema["x-custom"]).toBe("custom value");
});
});
describe("Unsupported schemas", () => {
it("should return undefined for unsupported schema types", () => {
const invalidSchema = { notASchema: true };
const result = schemaToJsonSchema(invalidSchema);
expect(result).toBeUndefined();
});
it("should return undefined for plain functions", () => {
const fn = (value: unknown) => typeof value === "string";
const result = schemaToJsonSchema(fn);
expect(result).toBeUndefined();
});
});
});
describe("canConvertSchema", () => {
it("should return true for supported schemas", () => {
expect(canConvertSchema(z.string())).toBe(true);
expect(canConvertSchema(y.string())).toBe(true);
expect(canConvertSchema(type("string"))).toBe(true);
expect(canConvertSchema(Schema.String)).toBe(true);
expect(canConvertSchema(Type.String())).toBe(true);
});
it("should return false for unsupported schemas", () => {
expect(canConvertSchema({ notASchema: true })).toBe(false);
expect(canConvertSchema(() => true)).toBe(false);
});
});
describe("detectSchemaType", () => {
it("should detect Zod schemas", () => {
expect(detectSchemaType(z.string())).toBe("zod");
});
it("should detect Yup schemas", () => {
expect(detectSchemaType(y.string())).toBe("yup");
});
it("should detect ArkType schemas", () => {
expect(detectSchemaType(type("string"))).toBe("arktype");
});
it("should detect Effect schemas", () => {
expect(detectSchemaType(Schema.String)).toBe("effect");
});
it("should detect TypeBox schemas", () => {
expect(detectSchemaType(Type.String())).toBe("typebox");
});
it("should return unknown for unsupported schemas", () => {
expect(detectSchemaType({ notASchema: true })).toBe("unknown");
});
});
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../.configs/tsconfig.base.json",
"references": [
{
"path": "./tsconfig.src.json"
},
{
"path": "./tsconfig.test.json"
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts"],
"compilerOptions": {
"isolatedDeclarations": false,
"composite": true,
"sourceMap": true,
"customConditions": ["@triggerdotdev/source"]
}
}
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"include": ["./test/**/*.ts"],
"references": [{ "path": "./tsconfig.src.json" }],
"compilerOptions": {
"isolatedDeclarations": false,
"composite": true,
"sourceMap": true,
"types": ["vitest/globals"]
}
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
+2 -2
View File
@@ -75,10 +75,10 @@
"tshy": "^3.0.2",
"tsx": "4.17.0",
"typed-emitter": "^2.1.0",
"zod": "3.23.8"
"zod": "3.25.76"
},
"peerDependencies": {
"zod": "^3.0.0",
"zod": "^3.0.0 || ^4.0.0",
"ai": "^4.2.0"
},
"peerDependenciesMeta": {
+1
View File
@@ -13,6 +13,7 @@ export * from "./metadata.js";
export * from "./timeout.js";
export * from "./webhooks.js";
export * from "./locals.js";
export * from "./schemas.js";
export type { Context };
import type { Context } from "./shared.js";
+2
View File
@@ -0,0 +1,2 @@
// Re-export JSON Schema types for user convenience
export type { JSONSchema } from "@trigger.dev/core/v3";
+25 -1
View File
@@ -76,6 +76,7 @@ import type {
TaskBatchOutputHandle,
TaskIdentifier,
TaskOptions,
TaskOptionsWithSchema,
TaskOutput,
TaskOutputHandle,
TaskPayload,
@@ -128,6 +129,16 @@ export function queue(options: QueueOptions): Queue {
return options;
}
// Overload: when payloadSchema is provided, payload type should be any
export function createTask<
TIdentifier extends string,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: TaskOptionsWithSchema<TIdentifier, TOutput, TInitOutput>
): Task<TIdentifier, any, TOutput>;
// Overload: normal case without payloadSchema
export function createTask<
TIdentifier extends string,
TInput = void,
@@ -135,7 +146,18 @@ export function createTask<
TInitOutput extends InitOutput = any,
>(
params: TaskOptions<TIdentifier, TInput, TOutput, TInitOutput>
): Task<TIdentifier, TInput, TOutput> {
): Task<TIdentifier, TInput, TOutput>;
export function createTask<
TIdentifier extends string,
TInput = void,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params:
| TaskOptions<TIdentifier, TInput, TOutput, TInitOutput>
| TaskOptionsWithSchema<TIdentifier, TOutput, TInitOutput>
): Task<TIdentifier, TInput, TOutput> | Task<TIdentifier, any, TOutput> {
const task: Task<TIdentifier, TInput, TOutput> = {
id: params.id,
description: params.description,
@@ -204,6 +226,7 @@ export function createTask<
retry: params.retry ? { ...defaultRetryOptions, ...params.retry } : undefined,
machine: typeof params.machine === "string" ? { preset: params.machine } : params.machine,
maxDuration: params.maxDuration,
payloadSchema: params.jsonSchema,
fns: {
run: params.run,
},
@@ -338,6 +361,7 @@ export function createSchemaTask<
run: params.run,
parsePayload,
},
schema: params.schema,
});
const queue = params.queue;
+855 -452
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -42,7 +42,7 @@
"react-markdown": "^10.1.0",
"tailwind-merge": "^3.1.0",
"tw-animate-css": "^1.2.4",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
+1 -1
View File
@@ -33,7 +33,7 @@
"react-dom": "^19.0.0",
"tailwind-merge": "^3.0.2",
"tw-animate-css": "^1.2.4",
"zod": "3.23.8",
"zod": "3.25.76",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
+4 -1
View File
@@ -8,10 +8,13 @@
"dependencies": {
"@trigger.dev/build": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"arktype": "^2.0.0",
"openai": "^4.97.0",
"puppeteer-core": "^24.15.0",
"replicate": "^1.0.1",
"zod": "3.23.8"
"yup": "^1.6.1",
"zod": "3.25.76",
"@sinclair/typebox": "^0.34.3"
},
"scripts": {
"dev": "trigger dev",
@@ -19,7 +19,7 @@ export const helloWorldTask = task({
env: process.env,
});
logger.debug("debug: Hello, world!", { payload });
logger.debug("debug: Hello, worlds!", { payload });
logger.info("info: Hello, world!", { payload });
logger.log("log: Hello, world!", { payload });
logger.warn("warn: Hello, world!", { payload });
@@ -0,0 +1,413 @@
import { task, schemaTask, logger, type JSONSchema } from "@trigger.dev/sdk/v3";
import { z } from "zod";
import * as y from "yup";
import { type } from "arktype";
import { Type, Static } from "@sinclair/typebox";
// ===========================================
// Example 1: Using schemaTask with Zod
// ===========================================
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150),
preferences: z
.object({
newsletter: z.boolean().default(false),
theme: z.enum(["light", "dark"]).default("light"),
})
.optional(),
});
export const processUserWithZod = schemaTask({
id: "json-schema-zod-example",
schema: userSchema,
run: async (payload, { ctx }) => {
// payload is fully typed based on the Zod schema
logger.info("Processing user with Zod schema", {
userId: payload.id,
userName: payload.name,
});
// The schema is automatically converted to JSON Schema and synced
return {
processed: true,
userId: payload.id,
welcomeMessage: `Welcome ${payload.name}!`,
};
},
});
// ===========================================
// Example 2: Using plain task with manual JSON Schema
// ===========================================
export const processOrderManualSchema = task({
id: "json-schema-manual-example",
// Manually provide JSON Schema for the payload
jsonSchema: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
title: "Order Processing Request",
description: "Schema for processing customer orders",
properties: {
orderId: {
type: "string",
pattern: "^ORD-[0-9]+$",
description: "Order ID in format ORD-XXXXX",
},
customerId: {
type: "string",
format: "uuid",
},
items: {
type: "array",
minItems: 1,
items: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "integer", minimum: 1 },
price: { type: "number", minimum: 0, multipleOf: 0.01 },
},
required: ["productId", "quantity", "price"],
additionalProperties: false,
},
},
totalAmount: {
type: "number",
minimum: 0,
multipleOf: 0.01,
},
status: {
type: "string",
enum: ["pending", "processing", "shipped", "delivered"],
default: "pending",
},
},
required: ["orderId", "customerId", "items", "totalAmount"],
additionalProperties: false,
} satisfies JSONSchema,
run: async (payload, { ctx }) => {
logger.info("Processing order with manual JSON Schema", {
orderId: payload.orderId,
});
// Note: With plain tasks, the payload is typed as 'any'
// The JSON Schema will be used for documentation and validation on the server
return {
processed: true,
orderId: payload.orderId,
status: "processing",
};
},
});
// ===========================================
// Example 3: Using schemaTask with Yup
// ===========================================
const productSchema = y.object({
sku: y
.string()
.required()
.matches(/^[A-Z]{3}-[0-9]{5}$/),
name: y.string().required().min(3).max(100),
description: y.string().max(500),
price: y.number().required().positive(),
categories: y.array().of(y.string()).min(1).required(),
inStock: y.boolean().default(true),
});
export const processProductWithYup = schemaTask({
id: "json-schema-yup-example",
schema: productSchema,
run: async (payload, { ctx }) => {
logger.info("Processing product with Yup schema", {
sku: payload.sku,
name: payload.name,
});
return {
processed: true,
sku: payload.sku,
message: `Product ${payload.name} has been processed`,
};
},
});
// ===========================================
// Example 4: Using schemaTask with ArkType
// ===========================================
const invoiceSchema = type({
invoiceNumber: "string",
date: "Date",
dueDate: "Date",
"discount?": "number",
lineItems: [
{
description: "string",
quantity: "number",
unitPrice: "number",
},
],
customer: {
id: "string",
name: "string",
"taxId?": "string",
},
});
export const processInvoiceWithArkType = schemaTask({
id: "json-schema-arktype-example",
schema: invoiceSchema,
run: async (payload, { ctx }) => {
logger.info("Processing invoice with ArkType schema", {
invoiceNumber: payload.invoiceNumber,
customerName: payload.customer.name,
});
const total = payload.lineItems.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
const discount = payload.discount || 0;
const finalAmount = total * (1 - discount / 100);
return {
processed: true,
invoiceNumber: payload.invoiceNumber,
totalAmount: finalAmount,
};
},
});
// ===========================================
// Example 5: Using TypeBox (already JSON Schema)
// ===========================================
const eventSchema = Type.Object({
eventId: Type.String({ format: "uuid" }),
eventType: Type.Union([
Type.Literal("user.created"),
Type.Literal("user.updated"),
Type.Literal("user.deleted"),
Type.Literal("order.placed"),
Type.Literal("order.shipped"),
]),
timestamp: Type.Integer({ minimum: 0 }),
userId: Type.String(),
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
payload: Type.Unknown(),
});
type EventType = Static<typeof eventSchema>;
export const processEventWithTypeBox = task({
id: "json-schema-typebox-example",
// TypeBox schemas are already JSON Schema compliant
jsonSchema: eventSchema,
run: async (payload: EventType, { ctx }) => {
// Cast to get TypeScript type safety
const event = payload;
logger.info("Processing event with TypeBox schema", {
eventId: event.eventId,
eventType: event.eventType,
userId: event.userId,
});
// Handle different event types
switch (event.eventType) {
case "user.created":
logger.info("New user created", { userId: event.userId });
break;
case "order.placed":
logger.info("Order placed", { userId: event.userId });
break;
default:
logger.info("Event processed", { eventType: event.eventType });
}
return {
processed: true,
eventId: event.eventId,
eventType: event.eventType,
};
},
});
// ===========================================
// Example 6: Using plain task with a Zod schema
// ===========================================
// If you need to use a plain task but have a Zod schema,
// you should use schemaTask instead for better DX.
// This example shows what NOT to do:
const notificationSchema = z.object({
recipientId: z.string(),
type: z.enum(["email", "sms", "push"]),
subject: z.string().optional(),
message: z.string(),
priority: z.enum(["low", "normal", "high"]).default("normal"),
scheduledFor: z.date().optional(),
metadata: z.record(z.unknown()).optional(),
});
// ❌ Don't do this - use schemaTask instead!
export const sendNotificationBadExample = task({
id: "json-schema-dont-do-this",
run: async (payload, { ctx }) => {
// You'd have to manually validate
const notification = notificationSchema.parse(payload);
logger.info("This is not ideal - use schemaTask instead!");
return { sent: true };
},
});
// ✅ Do this instead - much better!
export const sendNotificationGoodExample = schemaTask({
id: "json-schema-do-this-instead",
schema: notificationSchema,
run: async (notification, { ctx }) => {
// notification is already validated and typed!
logger.info("Sending notification", {
recipientId: notification.recipientId,
type: notification.type,
priority: notification.priority,
});
// Simulate sending notification
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
sent: true,
notificationId: ctx.run.id,
recipientId: notification.recipientId,
type: notification.type,
};
},
});
// ===========================================
// Example 7: Complex nested schema with references
// ===========================================
const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().default("US"),
});
const companySchema = z.object({
companyId: z.string().uuid(),
name: z.string(),
taxId: z.string().optional(),
addresses: z.object({
billing: addressSchema,
shipping: addressSchema.optional(),
}),
contacts: z
.array(
z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
role: z.enum(["primary", "billing", "technical"]),
})
)
.min(1),
settings: z.object({
invoicePrefix: z.string().default("INV"),
paymentTerms: z.number().int().min(0).max(90).default(30),
currency: z.enum(["USD", "EUR", "GBP"]).default("USD"),
}),
});
export const processCompanyWithComplexSchema = schemaTask({
id: "json-schema-complex-example",
schema: companySchema,
maxDuration: 300, // 5 minutes
retry: {
maxAttempts: 3,
factor: 2,
},
run: async (payload, { ctx }) => {
logger.info("Processing company with complex schema", {
companyId: payload.companyId,
name: payload.name,
contactCount: payload.contacts.length,
});
// Process each contact
for (const contact of payload.contacts) {
logger.info("Processing contact", {
name: contact.name,
role: contact.role,
});
}
return {
processed: true,
companyId: payload.companyId,
name: payload.name,
primaryContact: payload.contacts.find((c) => c.role === "primary"),
};
},
});
// ===========================================
// Example 8: Demonstrating schema benefits
// ===========================================
export const triggerExamples = task({
id: "json-schema-trigger-examples",
run: async (_, { ctx }) => {
logger.info("Triggering various schema examples");
// Trigger Zod example - TypeScript will enforce correct payload
await processUserWithZod.trigger({
id: "550e8400-e29b-41d4-a716-446655440000",
name: "John Doe",
email: "john@example.com",
age: 30,
preferences: {
newsletter: true,
theme: "dark",
},
});
// Trigger Yup example
await processProductWithYup.trigger({
sku: "ABC-12345",
name: "Premium Widget",
description: "A high-quality widget for all your needs",
price: 99.99,
categories: ["electronics", "gadgets"],
inStock: true,
});
// Trigger manual schema example (no compile-time validation)
await processOrderManualSchema.trigger({
orderId: "ORD-12345",
customerId: "550e8400-e29b-41d4-a716-446655440001",
items: [
{
productId: "PROD-001",
quantity: 2,
price: 29.99,
},
{
productId: "PROD-002",
quantity: 1,
price: 49.99,
},
],
totalAmount: 109.97,
status: "pending",
});
return {
message: "All examples triggered successfully",
timestamp: new Date().toISOString(),
};
},
});
@@ -0,0 +1,343 @@
import { task, schemaTask, logger, type JSONSchema } from "@trigger.dev/sdk/v3";
import { z } from "zod";
// ===========================================
// Example: Webhook Handler with Schema Validation
// ===========================================
// Define schemas for different webhook event types
const baseWebhookSchema = z.object({
id: z.string(),
timestamp: z.string().datetime(),
type: z.string(),
version: z.literal("1.0"),
});
// Payment webhook events
const paymentEventSchema = baseWebhookSchema.extend({
type: z.literal("payment"),
data: z.object({
paymentId: z.string(),
amount: z.number().positive(),
currency: z.string().length(3),
status: z.enum(["pending", "processing", "completed", "failed"]),
customerId: z.string(),
paymentMethod: z.object({
type: z.enum(["card", "bank_transfer", "paypal"]),
last4: z.string().optional(),
}),
metadata: z.record(z.string()).optional(),
}),
});
// Customer webhook events
const customerEventSchema = baseWebhookSchema.extend({
type: z.literal("customer"),
data: z.object({
customerId: z.string(),
action: z.enum(["created", "updated", "deleted"]),
email: z.string().email(),
name: z.string(),
subscription: z.object({
status: z.enum(["active", "cancelled", "past_due"]),
plan: z.string(),
}).optional(),
}),
});
// Union of all webhook types
const webhookSchema = z.discriminatedUnion("type", [
paymentEventSchema,
customerEventSchema,
]);
export const handleWebhook = schemaTask({
id: "handle-webhook",
schema: webhookSchema,
run: async (payload, { ctx }) => {
logger.info("Processing webhook", {
id: payload.id,
type: payload.type,
timestamp: payload.timestamp,
});
// TypeScript knows the exact shape based on the discriminated union
switch (payload.type) {
case "payment":
logger.info("Payment event received", {
paymentId: payload.data.paymentId,
amount: payload.data.amount,
status: payload.data.status,
});
if (payload.data.status === "completed") {
// Trigger order fulfillment
await fulfillOrder.trigger({
customerId: payload.data.customerId,
paymentId: payload.data.paymentId,
amount: payload.data.amount,
});
}
break;
case "customer":
logger.info("Customer event received", {
customerId: payload.data.customerId,
action: payload.data.action,
});
if (payload.data.action === "created") {
// Send welcome email
await sendWelcomeEmail.trigger({
email: payload.data.email,
name: payload.data.name,
});
}
break;
}
return {
processed: true,
eventId: payload.id,
eventType: payload.type,
};
},
});
// ===========================================
// Example: External API Integration
// ===========================================
// Schema for making API requests to a third-party service
const apiRequestSchema = z.object({
endpoint: z.enum(["/users", "/products", "/orders"]),
method: z.enum(["GET", "POST", "PUT", "DELETE"]),
params: z.record(z.string()).optional(),
body: z.unknown().optional(),
headers: z.record(z.string()).optional(),
retryOnError: z.boolean().default(true),
});
// Response schemas for different endpoints
const userResponseSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
createdAt: z.string().datetime(),
});
const productResponseSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number(),
inStock: z.boolean(),
});
export const callExternalApi = schemaTask({
id: "call-external-api",
schema: apiRequestSchema,
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
},
run: async (payload, { ctx }) => {
logger.info("Making API request", {
endpoint: payload.endpoint,
method: payload.method,
});
// Simulate API call
const response = await makeApiCall(payload);
// Validate response based on endpoint
let validatedResponse;
switch (payload.endpoint) {
case "/users":
validatedResponse = userResponseSchema.parse(response);
break;
case "/products":
validatedResponse = productResponseSchema.parse(response);
break;
default:
validatedResponse = response;
}
return {
success: true,
endpoint: payload.endpoint,
response: validatedResponse,
};
},
});
// Helper function to simulate API calls
async function makeApiCall(request: z.infer<typeof apiRequestSchema>) {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100));
// Return mock data based on endpoint
switch (request.endpoint) {
case "/users":
return {
id: "user_123",
email: "user@example.com",
name: "John Doe",
createdAt: new Date().toISOString(),
};
case "/products":
return {
id: "prod_456",
name: "Premium Widget",
price: 99.99,
inStock: true,
};
default:
return { message: "Success" };
}
}
// ===========================================
// Example: Batch Processing with Validation
// ===========================================
const batchItemSchema = z.object({
id: z.string(),
operation: z.enum(["create", "update", "delete"]),
resourceType: z.enum(["user", "product", "order"]),
data: z.record(z.unknown()),
});
const batchRequestSchema = z.object({
batchId: z.string(),
items: z.array(batchItemSchema).min(1).max(100),
options: z.object({
stopOnError: z.boolean().default(false),
parallel: z.boolean().default(true),
maxConcurrency: z.number().int().min(1).max(10).default(5),
}).default({}),
});
export const processBatch = schemaTask({
id: "process-batch",
schema: batchRequestSchema,
maxDuration: 300, // 5 minutes for large batches
run: async (payload, { ctx }) => {
logger.info("Processing batch", {
batchId: payload.batchId,
itemCount: payload.items.length,
parallel: payload.options.parallel,
});
const results = [];
const errors = [];
if (payload.options.parallel) {
// Process items in parallel with concurrency limit
const chunks = chunkArray(payload.items, payload.options.maxConcurrency);
for (const chunk of chunks) {
const chunkResults = await Promise.allSettled(
chunk.map(item => processItem(item))
);
chunkResults.forEach((result, index) => {
if (result.status === "fulfilled") {
results.push(result.value);
} else {
errors.push({
item: chunk[index],
error: result.reason,
});
if (payload.options.stopOnError) {
throw new Error(`Batch processing stopped due to error in item ${chunk[index].id}`);
}
}
});
}
} else {
// Process items sequentially
for (const item of payload.items) {
try {
const result = await processItem(item);
results.push(result);
} catch (error) {
errors.push({ item, error });
if (payload.options.stopOnError) {
throw new Error(`Batch processing stopped due to error in item ${item.id}`);
}
}
}
}
return {
batchId: payload.batchId,
processed: results.length,
failed: errors.length,
results,
errors,
};
},
});
async function processItem(item: z.infer<typeof batchItemSchema>) {
logger.info("Processing batch item", {
id: item.id,
operation: item.operation,
resourceType: item.resourceType,
});
// Simulate processing
await new Promise(resolve => setTimeout(resolve, 50));
return {
id: item.id,
success: true,
operation: item.operation,
resourceType: item.resourceType,
};
}
function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// ===========================================
// Helper Tasks
// ===========================================
const orderSchema = z.object({
customerId: z.string(),
paymentId: z.string(),
amount: z.number(),
});
export const fulfillOrder = schemaTask({
id: "fulfill-order",
schema: orderSchema,
run: async (payload, { ctx }) => {
logger.info("Fulfilling order", payload);
return { fulfilled: true };
},
});
const welcomeEmailSchema = z.object({
email: z.string().email(),
name: z.string(),
});
export const sendWelcomeEmail = schemaTask({
id: "send-welcome-email",
schema: welcomeEmailSchema,
run: async (payload, { ctx }) => {
logger.info("Sending welcome email", payload);
return { sent: true };
},
});
@@ -0,0 +1,235 @@
import { task, schemaTask, logger, type JSONSchema } from "@trigger.dev/sdk/v3";
import { z } from "zod";
// ===========================================
// The Two Main Approaches
// ===========================================
// Approach 1: Using schemaTask (Recommended)
// - Automatic JSON Schema conversion
// - Full TypeScript type safety
// - Runtime validation built-in
const emailSchema = z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
attachments: z
.array(
z.object({
filename: z.string(),
url: z.string().url(),
})
)
.optional(),
});
export const sendEmailSchemaTask = schemaTask({
id: "send-email-schema-task",
schema: emailSchema,
run: async (payload, { ctx }) => {
// payload is fully typed as:
// {
// to: string;
// subject: string;
// body: string;
// attachments?: Array<{ filename: string; url: string; }>;
// }
logger.info("Sending email", {
to: payload.to,
subject: payload.subject,
hasAttachments: !!payload.attachments?.length,
});
// Your email sending logic here...
return {
sent: true,
messageId: `msg_${ctx.run.id}`,
sentAt: new Date().toISOString(),
};
},
});
// Approach 2: Using plain task with payloadSchema
// - Manual JSON Schema definition
// - No automatic type inference (payload is 'any')
// - Good for when you already have JSON Schema definitions
export const sendEmailPlainTask = task({
id: "send-email-plain-task",
jsonSchema: {
type: "object",
properties: {
to: {
type: "string",
format: "email",
description: "Recipient email address",
},
subject: {
type: "string",
maxLength: 200,
},
body: {
type: "string",
},
attachments: {
type: "array",
items: {
type: "object",
properties: {
filename: { type: "string" },
url: { type: "string", format: "uri" },
},
required: ["filename", "url"],
},
},
},
required: ["to", "subject", "body"],
} satisfies JSONSchema, // Use 'satisfies' for type checking
run: async (payload, { ctx }) => {
// payload is typed as 'any' - you need to validate/cast it yourself
logger.info("Sending email", {
to: payload.to,
subject: payload.subject,
});
// Your email sending logic here...
return {
sent: true,
messageId: `msg_${ctx.run.id}`,
sentAt: new Date().toISOString(),
};
},
});
// ===========================================
// Benefits of JSON Schema
// ===========================================
// 1. Documentation - The schema is visible in the Trigger.dev dashboard
// 2. Validation - Invalid payloads are rejected before execution
// 3. Type Safety - With schemaTask, you get full TypeScript support
// 4. OpenAPI Generation - Can be used to generate API documentation
// 5. Client SDKs - Can generate typed clients for other languages
export const demonstrateBenefits = task({
id: "json-schema-benefits-demo",
run: async (_, { ctx }) => {
logger.info("Demonstrating JSON Schema benefits");
// With schemaTask, TypeScript prevents invalid payloads at compile time
try {
await sendEmailSchemaTask.trigger({
to: "user@example.com",
subject: "Welcome!",
body: "Thanks for signing up!",
// TypeScript error if you try to add invalid fields
// invalidField: "This would cause a TypeScript error",
});
} catch (error) {
logger.error("Failed to send email", { error });
}
// With plain task, validation happens at runtime
try {
await sendEmailPlainTask.trigger({
to: "not-an-email", // This will fail validation at runtime
subject: "Test",
body: "Test email",
});
} catch (error) {
logger.error("Failed validation", { error });
}
return { demonstrated: true };
},
});
// ===========================================
// Real-World Example: User Registration Flow
// ===========================================
const userRegistrationSchema = z.object({
email: z.string().email(),
username: z
.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/),
password: z.string().min(8),
profile: z.object({
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.string().optional(), // ISO date string
preferences: z
.object({
newsletter: z.boolean().default(false),
notifications: z.boolean().default(true),
})
.default({}),
}),
referralCode: z.string().optional(),
});
export const registerUser = schemaTask({
id: "register-user",
schema: userRegistrationSchema,
retry: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
},
run: async (payload, { ctx }) => {
logger.info("Registering new user", {
email: payload.email,
username: payload.username,
});
// Step 1: Validate uniqueness
logger.info("Checking if user exists");
// ... database check logic ...
// Step 2: Create user account
logger.info("Creating user account");
const userId = `user_${Date.now()}`;
// ... user creation logic ...
// Step 3: Send welcome email
await sendEmailSchemaTask.trigger({
to: payload.email,
subject: `Welcome to our platform, ${payload.profile.firstName}!`,
body: `Hi ${payload.profile.firstName},\n\nThanks for joining us...`,
});
// Step 4: Apply referral code if provided
if (payload.referralCode) {
logger.info("Processing referral code", { code: payload.referralCode });
// ... referral logic ...
}
return {
success: true,
userId,
username: payload.username,
welcomeEmailSent: true,
};
},
});
// ===========================================
// When to Use Each Approach
// ===========================================
/*
Use schemaTask when:
- You're already using Zod, Yup, ArkType, etc. in your codebase
- You want TypeScript type inference
- You want runtime validation handled automatically
- You're building new tasks from scratch
Use plain task with payloadSchema when:
- You have existing JSON Schema definitions
- You're migrating from another system that uses JSON Schema
- You need fine-grained control over the schema format
- You're working with generated schemas from OpenAPI/Swagger
*/
+118
View File
@@ -0,0 +1,118 @@
# JSON Schema Test Reference Project
This project demonstrates and tests the JSON schema functionality in Trigger.dev v3.
## Features Implemented
### 1. JSONSchema Type Export
- ✅ Proper `JSONSchema` type based on JSON Schema Draft 7
- ✅ Exported from `@trigger.dev/sdk/v3`
- ✅ Can be used with TypeScript's `satisfies` operator
### 2. Plain Task with payloadSchema
- ✅ Tasks accept a `payloadSchema` property
- ✅ Schema is stored and will be synced during indexing
- ✅ Type-safe schema definition
### 3. Schema Task with Automatic Conversion
-`schemaTask` automatically converts Zod schemas to JSON Schema
- ✅ Full TypeScript type inference from schema
- ✅ Runtime validation built-in
### 4. Type Safety
-`trigger()` and `triggerAndWait()` have proper type inference
- ✅ Batch operations maintain type safety
- ✅ Output types are properly inferred
### 5. Schema Conversion Package
-`@trigger.dev/schema-to-json` package created
- ✅ Supports multiple schema libraries (Zod, Yup, ArkType, etc.)
- ✅ Bundle-safe with dynamic imports
- ✅ Auto-initialized by SDK (no user configuration needed)
## Example Usage
```typescript
import { schemaTask, task, type JSONSchema } from "@trigger.dev/sdk/v3";
import { z } from "zod";
// Option 1: Using schemaTask with Zod (recommended)
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
export const mySchemaTask = schemaTask({
id: "my-schema-task",
schema: userSchema,
run: async (payload, { ctx }) => {
// payload is fully typed!
console.log(payload.id, payload.name, payload.email);
return { processed: true };
},
});
// Option 2: Using plain task with manual JSON schema
const jsonSchema: JSONSchema = {
type: "object",
properties: {
message: { type: "string" },
},
required: ["message"],
};
export const myPlainTask = task({
id: "my-plain-task",
payloadSchema: jsonSchema,
run: async (payload, { ctx }) => {
// payload is untyped, but schema is stored
return { received: payload.message };
},
});
```
## Architecture
1. **Core Package** (`@trigger.dev/core`):
- Defines `JSONSchema` type
- Includes `payloadSchema` in task metadata
2. **SDK Package** (`@trigger.dev/sdk`):
- Re-exports `JSONSchema` type
- Auto-initializes schema converters
- Registers `payloadSchema` during task creation
3. **Schema Conversion Package** (`@trigger.dev/schema-to-json`):
- Converts various schema libraries to JSON Schema
- Uses dynamic imports for bundle safety
- Encapsulated as implementation detail
4. **Webapp**:
- Saves `payloadSchema` to `BackgroundWorkerTask` model
- Schema available for API documentation, validation, etc.
## Testing
Run the integration test to verify all functionality:
```bash
npm run dev
# Then trigger the integration test task
```
The integration test covers:
- Plain task with JSON schema
- Zod schema conversion
- Complex nested schemas
- Trigger type safety
- Batch operations
- Error handling
## Benefits
1. **Documentation**: Schemas visible in Trigger.dev dashboard
2. **Validation**: Invalid payloads rejected before execution
3. **Type Safety**: Full TypeScript support with schemaTask
4. **API Generation**: Can generate OpenAPI specs
5. **Client SDKs**: Can generate typed clients for other languages
+27
View File
@@ -0,0 +1,27 @@
{
"name": "json-schema-test",
"version": "1.0.0",
"description": "Test project for JSON schema functionality",
"type": "module",
"scripts": {
"dev": "trigger.dev@beta dev",
"trigger:deploy": "trigger.dev@beta deploy",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/sdk": "workspace:*",
"zod": "3.22.3",
"@sinclair/typebox": "^0.34.3",
"superstruct": "^2.0.2",
"@effect/schema": "^0.75.5",
"effect": "^3.11.11",
"arktype": "^2.0.0",
"valibot": "^1.0.0-beta.8",
"runtypes": "^6.7.0",
"yup": "^1.6.1"
},
"devDependencies": {
"@types/node": "^20.14.8",
"typescript": "^5.7.2"
}
}
@@ -0,0 +1,431 @@
// Core functionality test - testing JSON schema implementation with minimal dependencies
import { schemaTask, task, type JSONSchema } from "@trigger.dev/sdk/v3";
import { z } from "zod";
// Test 1: Verify JSONSchema type is exported and usable
const manualSchema: JSONSchema = {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string", minLength: 1, maxLength: 100 },
email: { type: "string", format: "email" },
active: { type: "boolean" },
score: { type: "number", minimum: 0, maximum: 100 },
tags: {
type: "array",
items: { type: "string" },
maxItems: 10,
},
metadata: {
type: "object",
additionalProperties: true,
},
},
required: ["id", "name", "email"],
};
// Test 2: Plain task accepts jsonSchema
export const plainJsonSchemaTask = task({
id: "plain-json-schema-task",
jsonSchema: manualSchema,
run: async (payload: any, { ctx }) => {
// payload is any, but schema is properly stored
console.log("Received payload:", payload);
return {
taskId: ctx.task.id,
runId: ctx.run.id,
received: true,
// Manual type assertion needed with plain task
userId: payload.id as string,
userName: payload.name as string,
};
},
});
// Test 3: Zod schema with automatic conversion
const userSchema = z.object({
userId: z.string().uuid(),
userName: z.string().min(2).max(50),
userEmail: z.string().email(),
age: z.number().int().min(18).max(120),
preferences: z.object({
theme: z.enum(["light", "dark", "auto"]).default("auto"),
notifications: z.boolean().default(true),
language: z.string().default("en"),
}),
tags: z.array(z.string()).max(5).default([]),
createdAt: z.string().datetime().optional(),
});
export const zodSchemaTask = schemaTask({
id: "zod-schema-task",
schema: userSchema,
run: async (payload, { ctx }) => {
// Full type inference from Zod schema
console.log("Processing user:", payload.userName);
// All these are properly typed
const id: string = payload.userId;
const name: string = payload.userName;
const email: string = payload.userEmail;
const age: number = payload.age;
const theme: "light" | "dark" | "auto" = payload.preferences.theme;
const notifications: boolean = payload.preferences.notifications;
const tagCount: number = payload.tags.length;
return {
processedUserId: id,
processedUserName: name,
processedUserEmail: email,
userAge: age,
theme,
notificationsEnabled: notifications,
tagCount,
};
},
});
// Test 4: Complex nested schema
const orderSchema = z.object({
orderId: z.string(),
customerId: z.string(),
items: z
.array(
z.object({
productId: z.string(),
productName: z.string(),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
discount: z.number().min(0).max(100).default(0),
})
)
.min(1),
shippingAddress: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zipCode: z.string(),
country: z.string().default("US"),
}),
billingAddress: z
.object({
street: z.string(),
city: z.string(),
state: z.string(),
zipCode: z.string(),
country: z.string(),
})
.optional(),
paymentMethod: z.discriminatedUnion("type", [
z.object({
type: z.literal("credit_card"),
cardNumber: z.string().regex(/^\d{4}$/), // last 4 digits only
cardBrand: z.enum(["visa", "mastercard", "amex", "discover"]),
}),
z.object({
type: z.literal("paypal"),
paypalEmail: z.string().email(),
}),
z.object({
type: z.literal("bank_transfer"),
accountNumber: z.string(),
routingNumber: z.string(),
}),
]),
orderStatus: z
.enum(["pending", "processing", "shipped", "delivered", "cancelled"])
.default("pending"),
createdAt: z.string().datetime(),
notes: z.string().optional(),
});
export const complexOrderTask = schemaTask({
id: "complex-order-task",
schema: orderSchema,
run: async (payload, { ctx }) => {
// Deep nested type inference
const orderId = payload.orderId;
const firstItem = payload.items[0];
const productName = firstItem.productName;
const quantity = firstItem.quantity;
// Calculate totals with full type safety
const subtotal = payload.items.reduce((sum, item) => {
const itemTotal = item.quantity * item.unitPrice;
const discount = itemTotal * (item.discount / 100);
return sum + (itemTotal - discount);
}, 0);
// Discriminated union handling
let paymentSummary: string;
switch (payload.paymentMethod.type) {
case "credit_card":
paymentSummary = `${payload.paymentMethod.cardBrand} ending in ${payload.paymentMethod.cardNumber}`;
break;
case "paypal":
paymentSummary = `PayPal (${payload.paymentMethod.paypalEmail})`;
break;
case "bank_transfer":
paymentSummary = `Bank transfer ending in ${payload.paymentMethod.accountNumber.slice(-4)}`;
break;
}
// Optional field handling
const hasBillingAddress = !!payload.billingAddress;
const billingCity = payload.billingAddress?.city ?? payload.shippingAddress.city;
return {
orderId,
customerId: payload.customerId,
itemCount: payload.items.length,
subtotal,
status: payload.orderStatus,
paymentSummary,
shippingCity: payload.shippingAddress.city,
billingCity,
hasBillingAddress,
hasNotes: !!payload.notes,
};
},
});
// Test 5: Task trigger type safety
export const testTriggerTypeSafety = task({
id: "test-trigger-type-safety",
run: async (_, { ctx }) => {
console.log("Testing trigger type safety...");
// Valid trigger - should compile
const handle1 = await zodSchemaTask.trigger({
userId: "550e8400-e29b-41d4-a716-446655440000",
userName: "John Doe",
userEmail: "john@example.com",
age: 30,
preferences: {
theme: "dark",
notifications: false,
language: "es",
},
tags: ["customer", "premium"],
createdAt: new Date().toISOString(),
});
// Using defaults - should also compile
const handle2 = await zodSchemaTask.trigger({
userId: "550e8400-e29b-41d4-a716-446655440001",
userName: "Jane Smith",
userEmail: "jane@example.com",
age: 25,
preferences: {}, // Will use defaults
// tags will default to []
});
// Test triggerAndWait with result handling
const result = await zodSchemaTask.triggerAndWait({
userId: "550e8400-e29b-41d4-a716-446655440002",
userName: "Bob Wilson",
userEmail: "bob@example.com",
age: 45,
preferences: {
theme: "light",
},
});
if (result.ok) {
// Type-safe access to output
console.log("Processed user:", result.output.processedUserName);
console.log("User email:", result.output.processedUserEmail);
console.log("Theme:", result.output.theme);
return {
success: true,
processedUserId: result.output.processedUserId,
userName: result.output.processedUserName,
};
} else {
return {
success: false,
error: String(result.error),
};
}
},
});
// Test 6: Batch operations with type safety
export const testBatchOperations = task({
id: "test-batch-operations",
run: async (_, { ctx }) => {
console.log("Testing batch operations...");
// Batch trigger
const batchHandle = await zodSchemaTask.batchTrigger([
{
payload: {
userId: "batch-001",
userName: "Batch User 1",
userEmail: "batch1@example.com",
age: 20,
preferences: {
theme: "dark",
},
},
},
{
payload: {
userId: "batch-002",
userName: "Batch User 2",
userEmail: "batch2@example.com",
age: 30,
preferences: {
theme: "light",
notifications: false,
},
tags: ["batch", "test"],
},
},
]);
console.log(`Triggered batch ${batchHandle.batchId} with ${batchHandle.runCount} runs`);
// Batch trigger and wait
const batchResult = await zodSchemaTask.batchTriggerAndWait([
{
payload: {
userId: "batch-003",
userName: "Batch User 3",
userEmail: "batch3@example.com",
age: 40,
preferences: {},
},
},
{
payload: {
userId: "batch-004",
userName: "Batch User 4",
userEmail: "batch4@example.com",
age: 50,
preferences: {
language: "fr",
},
tags: ["batch", "wait"],
},
},
]);
// Process results with type safety
const processed = batchResult.runs.map((run) => {
if (run.ok) {
return {
success: true,
userId: run.output.processedUserId,
userName: run.output.processedUserName,
theme: run.output.theme,
};
} else {
return {
success: false,
runId: run.id,
error: String(run.error),
};
}
});
return {
batchId: batchResult.id,
totalRuns: batchResult.runs.length,
successfulRuns: processed.filter((p) => p.success).length,
processed,
};
},
});
// Test 7: Integration test - all features together
export const integrationTest = task({
id: "json-schema-integration-test",
run: async (_, { ctx }) => {
console.log("Running integration test...");
const results = {
plainTask: false,
zodTask: false,
complexTask: false,
triggerTypes: false,
batchOps: false,
};
try {
// Test plain JSON schema task
const plainResult = await plainJsonSchemaTask.trigger({
id: "test-001",
name: "Test User",
email: "test@example.com",
active: true,
score: 85,
tags: ["test"],
metadata: { source: "integration-test" },
});
results.plainTask = !!plainResult.id;
// Test Zod schema task
const zodResult = await zodSchemaTask
.triggerAndWait({
userId: "int-test-001",
userName: "Integration Test User",
userEmail: "integration@example.com",
age: 35,
preferences: {
theme: "auto",
},
})
.unwrap();
results.zodTask = zodResult.processedUserId === "int-test-001";
// Test complex schema
const complexResult = await complexOrderTask.trigger({
orderId: "order-int-001",
customerId: "cust-int-001",
items: [
{
productId: "prod-001",
productName: "Test Product",
quantity: 2,
unitPrice: 29.99,
discount: 10,
},
],
shippingAddress: {
street: "123 Test St",
city: "Test City",
state: "TC",
zipCode: "12345",
},
paymentMethod: {
type: "credit_card",
cardNumber: "1234",
cardBrand: "visa",
},
createdAt: new Date().toISOString(),
});
results.complexTask = !!complexResult.id;
// Test trigger type safety
const triggerResult = await testTriggerTypeSafety.triggerAndWait(undefined);
results.triggerTypes = triggerResult.ok && triggerResult.output.success;
// Test batch operations
const batchResult = await testBatchOperations.triggerAndWait(undefined);
results.batchOps = batchResult.ok && batchResult.output.successfulRuns > 0;
} catch (error) {
console.error("Integration test error:", error);
}
const allPassed = Object.values(results).every((r) => r);
return {
success: allPassed,
results,
message: allPassed ? "All JSON schema tests passed!" : "Some tests failed - check results",
};
},
});
@@ -0,0 +1,243 @@
// This file tests the core JSON schema functionality without external dependencies
import { schemaTask, task, type JSONSchema } from "@trigger.dev/sdk";
import { z } from "zod";
// Test 1: Basic type inference with schemaTask
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number(),
});
export const testZodTypeInference = schemaTask({
id: "test-zod-type-inference",
schema: userSchema,
run: async (payload, { ctx }) => {
// These should all be properly typed without explicit type annotations
const id = payload.id; // string
const name = payload.name; // string
const email = payload.email; // string
const age = payload.age; // number
// This would cause a TypeScript error if uncommented:
// const invalid = payload.nonExistentField;
return {
userId: id,
userName: name,
userEmail: email,
userAge: age,
};
},
});
// Test 2: JSONSchema type is properly exported and usable
const jsonSchemaExample = {
type: "object",
properties: {
message: { type: "string" },
count: { type: "integer" },
active: { type: "boolean" },
},
required: ["message", "count"],
} satisfies JSONSchema;
export const testJSONSchemaType = task({
id: "test-json-schema-type",
jsonSchema: jsonSchemaExample,
run: async (payload, { ctx }) => {
// payload is 'any' with plain task, but the schema is properly typed
return {
received: true,
message: payload.message,
count: payload.count,
active: payload.active ?? false,
};
},
});
// Test 3: Trigger type safety
export const testTriggerTypeSafety = task({
id: "test-trigger-type-safety",
run: async (_, { ctx }) => {
// This should compile with proper type inference
const handle1 = await testZodTypeInference.trigger({
id: "123",
name: "John Doe",
email: "john@example.com",
age: 30,
});
// This would cause TypeScript errors if uncommented:
// const handle2 = await testZodTypeInference.trigger({
// id: 123, // wrong type
// name: "Jane",
// email: "not-an-email", // invalid format (caught at runtime)
// age: "thirty", // wrong type
// });
// Test triggerAndWait
const result = await testZodTypeInference.triggerAndWait({
id: "456",
name: "Jane Smith",
email: "jane@example.com",
age: 25,
});
if (result.ok) {
// Type inference works on the output
const userId: string = result.output.userId;
const userName: string = result.output.userName;
const userEmail: string = result.output.userEmail;
const userAge: number = result.output.userAge;
return {
success: true,
userId,
userName,
userEmail,
userAge,
};
} else {
return {
success: false,
error: String(result.error),
};
}
},
});
// Test 4: Batch operations maintain type safety
export const testBatchTypeSafety = task({
id: "test-batch-type-safety",
run: async (_, { ctx }) => {
// Batch trigger with type safety
const batchHandle = await testZodTypeInference.batchTrigger([
{
payload: {
id: "1",
name: "User One",
email: "user1@example.com",
age: 20,
},
},
{
payload: {
id: "2",
name: "User Two",
email: "user2@example.com",
age: 30,
},
},
]);
// Batch trigger and wait
const batchResult = await testZodTypeInference.batchTriggerAndWait([
{
payload: {
id: "3",
name: "User Three",
email: "user3@example.com",
age: 40,
},
},
{
payload: {
id: "4",
name: "User Four",
email: "user4@example.com",
age: 50,
},
},
]);
// Process results with type safety
const successfulUsers: string[] = [];
const failedUsers: string[] = [];
for (const run of batchResult.runs) {
if (run.ok) {
// output is properly typed
successfulUsers.push(run.output.userId);
} else {
failedUsers.push(run.id);
}
}
return {
batchId: batchHandle.batchId,
batchRunCount: batchHandle.runCount,
successfulUsers,
failedUsers,
totalProcessed: batchResult.runs.length,
};
},
});
// Test 5: Complex nested schema
const complexSchema = z.object({
order: z.object({
id: z.string(),
items: z.array(
z.object({
productId: z.string(),
quantity: z.number(),
price: z.number(),
})
),
customer: z.object({
id: z.string(),
email: z.string().email(),
address: z
.object({
street: z.string(),
city: z.string(),
country: z.string(),
})
.optional(),
}),
}),
metadata: z.record(z.unknown()).optional(),
});
export const testComplexSchema = schemaTask({
id: "test-complex-schema",
schema: complexSchema,
run: async (payload, { ctx }) => {
// Deep type inference works
const orderId = payload.order.id;
const firstItem = payload.order.items[0];
const quantity = firstItem?.quantity ?? 0;
const customerEmail = payload.order.customer.email;
const city = payload.order.customer.address?.city;
// Calculate total
const total = payload.order.items.reduce((sum, item) => sum + item.quantity * item.price, 0);
return {
orderId,
customerEmail,
itemCount: payload.order.items.length,
total,
hasAddress: !!payload.order.customer.address,
city: city ?? "Unknown",
};
},
});
// Test 6: Verify that JSON schema is properly set during task registration
export const verifySchemaRegistration = task({
id: "verify-schema-registration",
run: async (_, { ctx }) => {
// This test verifies that when we create tasks with schemas,
// they properly register the payloadSchema for syncing to the server
return {
test: "Schema registration",
message: "If this task runs, schema registration is working",
// The actual verification happens during indexing when the CLI
// reads the task metadata and sees the payloadSchema field
};
},
});
@@ -0,0 +1,311 @@
import { schemaTask, task } from "@trigger.dev/sdk/v3";
import { z } from "zod";
// Define schemas for batch operations
const emailSchema = z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
priority: z.enum(["low", "normal", "high"]).default("normal"),
});
const smsSchema = z.object({
phoneNumber: z.string().regex(/^\+[1-9]\d{1,14}$/),
message: z.string().max(160),
});
const notificationSchema = z.object({
userId: z.string(),
title: z.string(),
message: z.string(),
type: z.enum(["info", "warning", "error", "success"]),
metadata: z.record(z.unknown()).optional(),
});
// Create schema tasks
export const sendEmail = schemaTask({
id: "send-email",
schema: emailSchema,
run: async (payload, { ctx }) => {
// Simulate sending email
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
return {
messageId: `email_${ctx.run.id}`,
sentAt: new Date().toISOString(),
to: payload.to,
subject: payload.subject,
priority: payload.priority,
};
},
});
export const sendSms = schemaTask({
id: "send-sms",
schema: smsSchema,
run: async (payload, { ctx }) => {
// Simulate sending SMS
await new Promise(resolve => setTimeout(resolve, Math.random() * 500));
return {
messageId: `sms_${ctx.run.id}`,
sentAt: new Date().toISOString(),
to: payload.phoneNumber,
characterCount: payload.message.length,
};
},
});
export const sendNotification = schemaTask({
id: "send-notification",
schema: notificationSchema,
run: async (payload, { ctx }) => {
// Simulate sending notification
await new Promise(resolve => setTimeout(resolve, Math.random() * 300));
return {
notificationId: `notif_${ctx.run.id}`,
sentAt: new Date().toISOString(),
userId: payload.userId,
type: payload.type,
delivered: Math.random() > 0.1, // 90% success rate
};
},
});
// Test batch operations with schema tasks
export const testBatchTrigger = task({
id: "test-batch-trigger",
run: async (_, { ctx }) => {
// Batch trigger emails
const emailBatch = await sendEmail.batchTrigger([
{
payload: {
to: "user1@example.com",
subject: "Welcome!",
body: "Welcome to our service.",
priority: "high",
},
},
{
payload: {
to: "user2@example.com",
subject: "Weekly Update",
body: "Here's your weekly update.",
// priority will default to "normal"
},
},
{
payload: {
to: "user3@example.com",
subject: "Special Offer",
body: "Check out our special offer!",
priority: "low",
},
},
]);
// Batch trigger SMS messages
const smsBatch = await sendSms.batchTrigger([
{
payload: {
phoneNumber: "+1234567890",
message: "Your verification code is 123456",
},
},
{
payload: {
phoneNumber: "+9876543210",
message: "Appointment reminder: Tomorrow at 2PM",
},
},
]);
return {
emailBatchId: emailBatch.batchId,
emailCount: emailBatch.runCount,
smsBatchId: smsBatch.batchId,
smsCount: smsBatch.runCount,
};
},
});
// Test batch trigger and wait
export const testBatchTriggerAndWait = task({
id: "test-batch-trigger-and-wait",
run: async (_, { ctx }) => {
// Batch trigger and wait for notifications
const notificationResults = await sendNotification.batchTriggerAndWait([
{
payload: {
userId: "user123",
title: "Info",
message: "This is an informational message",
type: "info",
},
},
{
payload: {
userId: "user456",
title: "Warning",
message: "This is a warning message",
type: "warning",
metadata: {
source: "system",
priority: "medium",
},
},
},
{
payload: {
userId: "user789",
title: "Success",
message: "Operation completed successfully",
type: "success",
},
},
]);
// Process results
const successCount = notificationResults.runs.filter(run => run.ok).length;
const failureCount = notificationResults.runs.filter(run => !run.ok).length;
const deliveredCount = notificationResults.runs
.filter(run => run.ok && run.output.delivered)
.length;
// Collect all notification IDs
const notificationIds = notificationResults.runs
.filter(run => run.ok)
.map(run => run.output.notificationId);
// Type safety check - these should all be properly typed
for (const run of notificationResults.runs) {
if (run.ok) {
const notifId: string = run.output.notificationId;
const sentAt: string = run.output.sentAt;
const userId: string = run.output.userId;
const type: "info" | "warning" | "error" | "success" = run.output.type;
const delivered: boolean = run.output.delivered;
}
}
return {
batchId: notificationResults.id,
totalRuns: notificationResults.runs.length,
successCount,
failureCount,
deliveredCount,
notificationIds,
};
},
});
// Test mixed batch operations
export const testMixedBatchOperations = task({
id: "test-mixed-batch-operations",
run: async (_, { ctx }) => {
// Trigger different types of messages for the same user
const results = await Promise.all([
// Send welcome email
sendEmail.trigger({
to: "newuser@example.com",
subject: "Welcome to our platform!",
body: "Thanks for signing up. Here's what you need to know...",
priority: "high",
}),
// Send SMS verification
sendSms.trigger({
phoneNumber: "+1234567890",
message: "Welcome! Your verification code is 789012",
}),
// Send in-app notification
sendNotification.trigger({
userId: "newuser123",
title: "Account Created",
message: "Your account has been successfully created",
type: "success",
metadata: {
accountType: "premium",
referralCode: "WELCOME2024",
},
}),
]);
// Wait for specific tasks using triggerAndWait
const criticalEmail = await sendEmail.triggerAndWait({
to: "admin@example.com",
subject: "New User Alert",
body: "A new premium user has signed up",
priority: "high",
});
if (criticalEmail.ok) {
const messageId: string = criticalEmail.output.messageId;
const sentAt: string = criticalEmail.output.sentAt;
return {
allMessagesSent: true,
emailId: results[0].id,
smsId: results[1].id,
notificationId: results[2].id,
criticalEmailId: messageId,
criticalEmailSentAt: sentAt,
};
} else {
return {
allMessagesSent: false,
error: "Failed to send critical email",
};
}
},
});
// Test error handling in batch operations
export const testBatchErrorHandling = task({
id: "test-batch-error-handling",
run: async (_, { ctx }) => {
// Create a batch with some invalid data to test error handling
const results = await sendEmail.batchTriggerAndWait([
{
payload: {
to: "valid@example.com",
subject: "Valid Email",
body: "This should succeed",
},
},
{
payload: {
to: "another.valid@example.com",
subject: "Another Valid Email",
body: "This should also succeed",
priority: "normal",
},
},
// Note: We can't actually create invalid payloads at compile time
// because TypeScript prevents it! This is the power of schema tasks.
// If we tried to add { to: "invalid-email", ... }, TypeScript would error
]);
// Process results with proper type safety
const report = {
totalAttempts: results.runs.length,
successful: [] as string[],
failed: [] as { id: string; error: string }[],
};
for (const run of results.runs) {
if (run.ok) {
report.successful.push(run.output.messageId);
} else {
report.failed.push({
id: run.id,
error: String(run.error),
});
}
}
return report;
},
});
@@ -0,0 +1,393 @@
import { schemaTask } from "@trigger.dev/sdk";
import { Type } from "@sinclair/typebox";
import {
array,
object,
string,
number,
boolean,
optional,
union,
literal,
record,
Infer,
} from "superstruct";
import * as S from "@effect/schema/Schema";
import { type } from "arktype";
import * as v from "valibot";
import * as rt from "runtypes";
// Test TypeBox schema
const typeBoxSchema = Type.Object({
id: Type.String({ pattern: "^[a-zA-Z0-9]+$" }),
title: Type.String({ minLength: 1, maxLength: 100 }),
content: Type.String({ minLength: 10 }),
author: Type.Object({
name: Type.String(),
email: Type.String({ format: "email" }),
role: Type.Union([Type.Literal("admin"), Type.Literal("editor"), Type.Literal("viewer")]),
}),
tags: Type.Array(Type.String(), { minItems: 1, maxItems: 5 }),
published: Type.Boolean(),
publishedAt: Type.Optional(Type.String({ format: "date-time" })),
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
});
export const typeBoxTask = schemaTask({
id: "typebox-schema-task",
schema: typeBoxSchema,
run: async (payload, { ctx }) => {
// TypeBox provides static type inference
const id: string = payload.id;
const title: string = payload.title;
const authorEmail: string = payload.author.email;
const role: "admin" | "editor" | "viewer" = payload.author.role;
const tagCount = payload.tags.length;
const isPublished: boolean = payload.published;
return {
documentId: id,
title,
authorEmail,
role,
tagCount,
status: isPublished ? "published" : "draft",
};
},
});
// Test Superstruct schema
const superstructSchema = object({
transaction: object({
id: string(),
amount: number(),
currency: union([literal("USD"), literal("EUR"), literal("GBP")]),
type: union([literal("credit"), literal("debit")]),
description: optional(string()),
tags: optional(array(string())),
metadata: optional(record(string(), string())),
}),
account: object({
accountId: string(),
balance: number(),
overdraftLimit: optional(number()),
}),
timestamp: string(),
approved: boolean(),
});
type SuperstructTransaction = Infer<typeof superstructSchema>;
export const superstructTask = schemaTask({
id: "superstruct-schema-task",
schema: superstructSchema,
run: async (payload: SuperstructTransaction, { ctx }) => {
// Superstruct infers types correctly
const transactionId = payload.transaction.id;
const amount = payload.transaction.amount;
const currency = payload.transaction.currency;
const accountBalance = payload.account.balance;
const isApproved = payload.approved;
const newBalance =
payload.transaction.type === "credit" ? accountBalance + amount : accountBalance - amount;
return {
transactionId,
processedAt: new Date().toISOString(),
newBalance,
currency,
approved: isApproved,
requiresReview: newBalance < 0 && !payload.account.overdraftLimit,
};
},
});
// Test Effect Schema
const effectSchema = S.Struct({
event: S.Struct({
eventId: S.String,
eventType: S.Literal("click", "view", "purchase"),
timestamp: S.Date,
sessionId: S.String,
}),
user: S.Struct({
userId: S.String,
email: S.String,
}),
product: S.optional(
S.Struct({
productId: S.String,
name: S.String,
price: S.Number,
category: S.String,
})
),
location: S.optional(
S.Struct({
country: S.String,
city: S.optional(S.String),
region: S.optional(S.String),
})
),
});
type EffectEvent = S.Schema.Type<typeof effectSchema>;
export const effectSchemaTask = schemaTask({
id: "effect-schema-task",
schema: effectSchema,
run: async (payload, { ctx }) => {
// Effect Schema provides type safety
const eventId = payload.event.eventId;
const eventType = payload.event.eventType;
const userId = payload.user.userId;
const hasProduct = !!payload.product;
const productName = payload.product?.name;
const country = payload.location?.country;
return {
eventId,
eventType,
userId,
hasProduct,
productName,
country: country ?? "unknown",
processed: true,
};
},
});
// Test ArkType schema
const arkTypeSchema = type({
request: {
method: "'GET' | 'POST' | 'PUT' | 'DELETE'",
path: "string",
headers: "Record<string, string>",
"body?": "unknown",
"query?": "Record<string, string>",
},
response: {
status: "number",
"headers?": "Record<string, string>",
"body?": "unknown",
},
timing: {
start: "Date",
end: "Date",
duration: "number",
},
"metadata?": {
"ip?": "string",
"userAgent?": "string",
"referer?": "string",
},
});
export const arkTypeTask = schemaTask({
id: "arktype-schema-task",
schema: arkTypeSchema,
run: async (payload, { ctx }) => {
// ArkType infers types
const method = payload.request.method;
const path = payload.request.path;
const status = payload.response.status;
const duration = payload.timing.duration;
const hasBody = !!payload.request.body;
const userAgent = payload.metadata?.userAgent;
return {
logId: `log_${ctx.run.id}`,
method,
path,
status,
duration,
hasBody,
userAgent: userAgent ?? "unknown",
success: status >= 200 && status < 300,
};
},
});
// Test Valibot schema
const valibotSchema = v.object({
form: v.object({
name: v.pipe(v.string(), v.minLength(2), v.maxLength(50)),
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(150)),
website: v.optional(v.pipe(v.string(), v.url())),
bio: v.optional(v.pipe(v.string(), v.maxLength(500))),
interests: v.array(v.string()),
preferences: v.object({
theme: v.union([v.literal("light"), v.literal("dark"), v.literal("auto")]),
notifications: v.boolean(),
language: v.string(),
}),
}),
submittedAt: v.string(),
source: v.union([v.literal("web"), v.literal("mobile"), v.literal("api")]),
});
type ValibotForm = v.InferOutput<typeof valibotSchema>;
export const valibotTask = schemaTask({
id: "valibot-schema-task",
schema: valibotSchema,
run: async (payload: ValibotForm, { ctx }) => {
// Valibot provides type inference
const name = payload.form.name;
const email = payload.form.email;
const age = payload.form.age;
const hasWebsite = !!payload.form.website;
const theme = payload.form.preferences.theme;
const source = payload.source;
return {
submissionId: `sub_${ctx.run.id}`,
name,
email,
age,
hasWebsite,
theme,
source,
processed: true,
};
},
});
// Test Runtypes schema
const runtypesSchema = rt.Record({
payment: rt.Record({
paymentId: rt.String,
amount: rt.Number,
currency: rt.Union(rt.Literal("USD"), rt.Literal("EUR"), rt.Literal("GBP")),
method: rt.Union(
rt.Record({
type: rt.Literal("card"),
last4: rt.String,
brand: rt.String,
}),
rt.Record({
type: rt.Literal("bank"),
accountNumber: rt.String,
routingNumber: rt.String,
})
),
status: rt.Union(
rt.Literal("pending"),
rt.Literal("processing"),
rt.Literal("completed"),
rt.Literal("failed")
),
}),
customer: rt.Record({
customerId: rt.String,
email: rt.String,
name: rt.String,
}),
metadata: rt.Optional(rt.Dictionary(rt.Unknown)),
});
type RuntypesPayment = rt.Static<typeof runtypesSchema>;
export const runtypesTask = schemaTask({
id: "runtypes-schema-task",
schema: runtypesSchema,
run: async (payload: RuntypesPayment, { ctx }) => {
// Runtypes provides static types
const paymentId = payload.payment.paymentId;
const amount = payload.payment.amount;
const currency = payload.payment.currency;
const status = payload.payment.status;
const customerEmail = payload.customer.email;
// Discriminated union handling
const paymentDetails =
payload.payment.method.type === "card"
? `Card ending in ${payload.payment.method.last4}`
: `Bank account ${payload.payment.method.accountNumber}`;
return {
paymentId,
amount,
currency,
status,
customerEmail,
paymentDetails,
requiresAction: status === "pending" || status === "processing",
};
},
});
// Test task that triggers all schema tasks
export const testAllSchemas = schemaTask({
id: "test-all-schemas",
schema: z.object({ runAll: z.boolean() }),
run: async (payload, { ctx }) => {
const results = [];
// Test TypeBox
const typeBoxResult = await typeBoxTask.trigger({
id: "doc123",
title: "Test Document",
content: "This is a test document with sufficient content.",
author: {
name: "John Doe",
email: "john@example.com",
role: "editor",
},
tags: ["test", "sample"],
published: true,
publishedAt: new Date().toISOString(),
});
results.push({ task: "typebox", runId: typeBoxResult.id });
// Test Superstruct
const superstructResult = await superstructTask.trigger({
transaction: {
id: "txn123",
amount: 100.5,
currency: "USD",
type: "credit",
description: "Test transaction",
},
account: {
accountId: "acc456",
balance: 1000,
overdraftLimit: 500,
},
timestamp: new Date().toISOString(),
approved: true,
});
results.push({ task: "superstruct", runId: superstructResult.id });
// Test Effect Schema
const effectResult = await effectSchemaTask.trigger({
event: {
eventId: "evt789",
eventType: "purchase",
timestamp: new Date(),
sessionId: "sess123",
},
user: {
userId: "user456",
email: "user@example.com",
},
product: {
productId: "prod789",
name: "Test Product",
price: 29.99,
category: "Electronics",
},
});
results.push({ task: "effect", runId: effectResult.id });
return {
tested: results.length,
results,
};
},
});
// Import zod for the test task
import { z } from "zod";
@@ -0,0 +1,189 @@
import { schemaTask } from "@trigger.dev/sdk/v3";
import * as yup from "yup";
// Test Yup schema conversion
const contactSchema = yup.object({
firstName: yup.string().required().min(2).max(50),
lastName: yup.string().required().min(2).max(50),
email: yup.string().email().required(),
phone: yup.string().matches(/^[\d\s\-\+\(\)]+$/, "Invalid phone number").optional(),
age: yup.number().positive().integer().min(18).max(120),
preferences: yup.object({
contactMethod: yup.string().oneOf(["email", "phone", "sms"]).default("email"),
newsletter: yup.boolean().default(false),
language: yup.string().oneOf(["en", "es", "fr", "de"]).default("en"),
}).default({}),
address: yup.object({
street: yup.string().required(),
city: yup.string().required(),
state: yup.string().length(2).required(),
zip: yup.string().matches(/^\d{5}$/).required(),
}).optional(),
tags: yup.array().of(yup.string()).min(1).max(10),
});
export const yupSchemaTask = schemaTask({
id: "yup-schema-task",
schema: contactSchema,
run: async (payload, { ctx }) => {
// Type checking: payload should be inferred from Yup schema
const fullName = `${payload.firstName} ${payload.lastName}`;
const email: string = payload.email;
const phone: string | undefined = payload.phone;
const age: number = payload.age;
// Nested properties
const contactMethod = payload.preferences.contactMethod;
const newsletter: boolean = payload.preferences.newsletter;
// Optional nested object
const hasAddress = !!payload.address;
const city = payload.address?.city;
// Array
const tagCount = payload.tags?.length ?? 0;
return {
contactId: `contact_${ctx.run.id}`,
fullName,
email,
hasPhone: !!phone,
hasAddress,
tagCount,
preferredContact: contactMethod,
};
},
});
// Test complex Yup validation with conditional logic
const orderValidationSchema = yup.object({
orderType: yup.string().oneOf(["standard", "express", "same-day"]).required(),
items: yup.array().of(
yup.object({
sku: yup.string().required(),
quantity: yup.number().positive().integer().required(),
price: yup.number().positive().required(),
})
).min(1).required(),
shipping: yup.object().when("orderType", {
is: "standard",
then: (schema) => schema.shape({
method: yup.string().oneOf(["ground", "air"]).required(),
estimatedDays: yup.number().min(3).max(10).required(),
}),
otherwise: (schema) => schema.shape({
method: yup.string().oneOf(["priority", "express"]).required(),
estimatedDays: yup.number().min(1).max(2).required(),
}),
}),
discount: yup.object({
code: yup.string().optional(),
percentage: yup.number().min(0).max(100).when("code", {
is: (code: any) => !!code,
then: (schema) => schema.required(),
otherwise: (schema) => schema.optional(),
}),
}).optional(),
customerNotes: yup.string().max(500).optional(),
});
export const yupConditionalTask = schemaTask({
id: "yup-conditional-task",
schema: orderValidationSchema,
run: async (payload, { ctx }) => {
// Type inference with conditional validation
const orderType = payload.orderType;
const itemCount = payload.items.length;
const totalQuantity = payload.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = payload.items.reduce((sum, item) => sum + (item.quantity * item.price), 0);
// Shipping details based on order type
const shippingMethod = payload.shipping.method;
const estimatedDays = payload.shipping.estimatedDays;
// Optional discount
const hasDiscount = !!payload.discount?.code;
const discountPercentage = payload.discount?.percentage ?? 0;
const discountAmount = hasDiscount ? (totalPrice * discountPercentage / 100) : 0;
return {
orderId: `order_${ctx.run.id}`,
orderType,
itemCount,
totalQuantity,
subtotal: totalPrice,
discount: discountAmount,
total: totalPrice - discountAmount,
shipping: {
method: shippingMethod,
estimatedDelivery: new Date(Date.now() + estimatedDays * 24 * 60 * 60 * 1000).toISOString(),
},
};
},
});
// Test Yup with custom validation and transforms
const userRegistrationSchema = yup.object({
username: yup.string()
.required()
.min(3)
.max(20)
.matches(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores")
.test("no-reserved", "Username is reserved", (value) => {
const reserved = ["admin", "root", "system", "user"];
return !reserved.includes(value?.toLowerCase() ?? "");
}),
email: yup.string()
.email()
.required()
.test("email-domain", "Email domain not allowed", (value) => {
const blockedDomains = ["tempmail.com", "throwaway.email"];
const domain = value?.split("@")[1]?.toLowerCase();
return !blockedDomains.includes(domain ?? "");
}),
password: yup.string()
.required()
.min(8)
.matches(/[A-Z]/, "Password must contain at least one uppercase letter")
.matches(/[a-z]/, "Password must contain at least one lowercase letter")
.matches(/[0-9]/, "Password must contain at least one number")
.matches(/[^A-Za-z0-9]/, "Password must contain at least one special character"),
confirmPassword: yup.string()
.required()
.oneOf([yup.ref("password")], "Passwords must match"),
dateOfBirth: yup.date()
.required()
.max(new Date(), "Date of birth cannot be in the future")
.test("age", "Must be at least 18 years old", (value) => {
if (!value) return false;
const age = new Date().getFullYear() - value.getFullYear();
return age >= 18;
}),
termsAccepted: yup.boolean()
.required()
.oneOf([true], "You must accept the terms and conditions"),
});
export const yupCustomValidationTask = schemaTask({
id: "yup-custom-validation-task",
schema: userRegistrationSchema,
run: async (payload, { ctx }) => {
// All validations have passed if we get here
const username: string = payload.username;
const email: string = payload.email;
const dateOfBirth: Date = payload.dateOfBirth;
const termsAccepted: boolean = payload.termsAccepted;
// Calculate age
const age = new Date().getFullYear() - dateOfBirth.getFullYear();
return {
userId: `user_${ctx.run.id}`,
username,
email,
age,
registeredAt: new Date().toISOString(),
welcomeEmailRequired: true,
};
},
});
@@ -0,0 +1,288 @@
import { schemaTask, task, type JSONSchema } from "@trigger.dev/sdk/v3";
import { z } from "zod";
// Test 1: Basic Zod schema with schemaTask
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0).max(150),
isActive: z.boolean(),
roles: z.array(z.enum(["admin", "user", "guest"])),
metadata: z.record(z.unknown()).optional(),
});
export const zodSchemaTask = schemaTask({
id: "zod-schema-task",
schema: userSchema,
run: async (payload, { ctx }) => {
// Type checking: payload should be fully typed
const id: string = payload.id;
const name: string = payload.name;
const email: string = payload.email;
const age: number = payload.age;
const isActive: boolean = payload.isActive;
const roles: ("admin" | "user" | "guest")[] = payload.roles;
const metadata: Record<string, unknown> | undefined = payload.metadata;
return {
processed: true,
userId: payload.id,
userName: payload.name,
};
},
});
// Test 2: Complex nested Zod schema
const complexSchema = z.object({
order: z.object({
orderId: z.string().uuid(),
items: z.array(
z.object({
productId: z.string(),
quantity: z.number().positive(),
price: z.number().positive(),
discount: z.number().min(0).max(100).optional(),
})
),
customer: z.object({
customerId: z.string(),
email: z.string().email(),
shippingAddress: z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().default("US"),
}),
}),
paymentMethod: z.discriminatedUnion("type", [
z.object({
type: z.literal("credit_card"),
last4: z.string().length(4),
brand: z.enum(["visa", "mastercard", "amex"]),
}),
z.object({
type: z.literal("paypal"),
email: z.string().email(),
}),
]),
createdAt: z.string().datetime(),
status: z.enum(["pending", "processing", "shipped", "delivered", "cancelled"]),
}),
notes: z.string().optional(),
priority: z.number().int().min(1).max(5).default(3),
});
export const complexZodTask = schemaTask({
id: "complex-zod-task",
schema: complexSchema,
run: async (payload, { ctx }) => {
// Test type inference on nested properties
const orderId: string = payload.order.orderId;
const firstItem = payload.order.items[0];
const quantity: number = firstItem.quantity;
const customerEmail: string = payload.order.customer.email;
const zipCode: string = payload.order.customer.shippingAddress.zipCode;
// Discriminated union type checking
if (payload.order.paymentMethod.type === "credit_card") {
const brand: "visa" | "mastercard" | "amex" = payload.order.paymentMethod.brand;
const last4: string = payload.order.paymentMethod.last4;
} else {
const paypalEmail: string = payload.order.paymentMethod.email;
}
return {
orderId: payload.order.orderId,
itemCount: payload.order.items.length,
status: payload.order.status,
};
},
});
// Test 3: Plain task with manual JSON schema
const manualJsonSchema: JSONSchema = {
type: "object",
properties: {
taskId: { type: "string", pattern: "^task_[a-zA-Z0-9]+$" },
priority: { type: "integer", minimum: 1, maximum: 10 },
tags: {
type: "array",
items: { type: "string" },
minItems: 1,
maxItems: 5,
},
config: {
type: "object",
properties: {
timeout: { type: "number" },
retries: { type: "integer", minimum: 0 },
async: { type: "boolean" },
},
required: ["timeout", "retries"],
},
},
required: ["taskId", "priority"],
additionalProperties: false,
};
export const plainJsonSchemaTask = task({
id: "plain-json-schema-task",
jsonSchema: manualJsonSchema,
run: async (payload, { ctx }) => {
// With plain task, payload is 'any' so we need to manually type it
const taskId = payload.taskId as string;
const priority = payload.priority as number;
const tags = payload.tags as string[] | undefined;
const config = payload.config as
| { timeout: number; retries: number; async?: boolean }
| undefined;
return {
processed: true,
taskId,
priority,
hasConfig: !!config,
};
},
});
// Test 4: Testing trigger type safety
export const testTriggerTypeSafety = task({
id: "test-trigger-type-safety",
run: async (_, { ctx }) => {
// This should compile successfully with proper types
const result1 = await zodSchemaTask.trigger({
id: "user123",
name: "John Doe",
email: "john@example.com",
age: 30,
isActive: true,
roles: ["user", "admin"],
});
// This should show TypeScript errors if uncommented:
// const result2 = await zodSchemaTask.trigger({
// id: "user123",
// name: "John Doe",
// email: "not-an-email", // Invalid email
// age: "thirty", // Wrong type
// isActive: "yes", // Wrong type
// roles: ["superuser"], // Invalid enum value
// });
// Test complex schema trigger
const result3 = await complexZodTask.trigger({
order: {
orderId: "550e8400-e29b-41d4-a716-446655440000",
items: [
{
productId: "prod123",
quantity: 2,
price: 29.99,
discount: 10,
},
],
customer: {
customerId: "cust456",
email: "customer@example.com",
shippingAddress: {
street: "123 Main St",
city: "Anytown",
state: "CA",
zipCode: "12345",
country: "US",
},
},
paymentMethod: {
type: "credit_card",
last4: "1234",
brand: "visa",
},
createdAt: new Date().toISOString(),
status: "pending",
},
priority: 5,
});
return {
triggered: true,
runIds: [result1.id, result3.id],
};
},
});
// Test 5: Testing triggerAndWait with proper unwrap
export const testTriggerAndWait = task({
id: "test-trigger-and-wait",
run: async (_, { ctx }) => {
// Test type inference with triggerAndWait
const result = await zodSchemaTask.triggerAndWait({
id: "user456",
name: "Jane Smith",
email: "jane@example.com",
age: 25,
isActive: false,
roles: ["guest"],
metadata: {
source: "api",
version: "1.0",
},
});
if (result.ok) {
// result.output should be typed
const processed: boolean = result.output.processed;
const userId: string = result.output.userId;
const userName: string = result.output.userName;
return {
success: true,
processedUserId: userId,
processedUserName: userName,
};
} else {
return {
success: false,
error: String(result.error),
};
}
},
});
// Test 6: Using unwrap() method
export const testUnwrap = task({
id: "test-unwrap",
run: async (_, { ctx }) => {
try {
// Using unwrap() for cleaner code
const output = await zodSchemaTask
.triggerAndWait({
id: "user789",
name: "Bob Johnson",
email: "bob@example.com",
age: 35,
isActive: true,
roles: ["user"],
})
.unwrap();
// output is directly typed without needing to check result.ok
const processed: boolean = output.processed;
const userId: string = output.userId;
const userName: string = output.userName;
return {
unwrapped: true,
userId,
userName,
};
} catch (error) {
return {
unwrapped: false,
error: String(error),
};
}
},
});
@@ -0,0 +1,9 @@
import { defineConfig } from "@trigger.dev/sdk/v3";
export default defineConfig({
project: "json-schema-test",
retries: {
enabledInDev: false,
},
triggerDirectories: ["./src/trigger"],
});
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": false,
"noEmit": true,
"paths": {
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index.ts"],
"@trigger.dev/sdk/v3": ["../../packages/trigger-sdk/src/v3/index.ts"],
"@trigger.dev/core": ["../../packages/core/src/index.ts"],
"@trigger.dev/core/v3": ["../../packages/core/src/v3/index.ts"],
"@trigger.dev/schema-to-json": ["../../packages/schema-to-json/src/index.ts"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
+259
View File
@@ -0,0 +1,259 @@
// Standalone type test to verify JSON schema implementation
// This imports directly from the source files to test compilation
import { schemaTask, task } from "../../packages/trigger-sdk/src/v3/index.js";
import type { JSONSchema } from "../../packages/trigger-sdk/src/v3/index.js";
import { z } from "zod";
// Test 1: JSONSchema type is properly exported
const testJsonSchemaType: JSONSchema = {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string", minLength: 1 },
age: { type: "integer", minimum: 0, maximum: 150 },
email: { type: "string", format: "email" },
tags: {
type: "array",
items: { type: "string" },
minItems: 0,
maxItems: 10,
},
active: { type: "boolean" },
metadata: {
type: "object",
additionalProperties: true,
},
},
required: ["id", "name", "email"],
additionalProperties: false,
};
// Test 2: Plain task accepts JSONSchema type
const plainTask = task({
id: "plain-task-with-schema",
payloadSchema: testJsonSchemaType, // This should compile without errors
run: async (payload, { ctx }) => {
return { processed: true };
},
});
// Test 3: Schema task with Zod
const zodSchema = z.object({
userId: z.string(),
userName: z.string(),
userEmail: z.string().email(),
isActive: z.boolean(),
score: z.number(),
tags: z.array(z.string()),
metadata: z.record(z.unknown()).optional(),
});
const zodTask = schemaTask({
id: "zod-schema-task",
schema: zodSchema,
run: async (payload, { ctx }) => {
// Type checking - all these should be properly typed
const userId: string = payload.userId;
const userName: string = payload.userName;
const userEmail: string = payload.userEmail;
const isActive: boolean = payload.isActive;
const score: number = payload.score;
const tags: string[] = payload.tags;
const metadata: Record<string, unknown> | undefined = payload.metadata;
return {
processedUserId: userId,
processedUserName: userName,
tagCount: tags.length,
};
},
});
// Test 4: Complex nested schemas
const nestedSchema = z.object({
order: z.object({
orderId: z.string().uuid(),
items: z.array(z.object({
itemId: z.string(),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
})),
customer: z.object({
customerId: z.string(),
email: z.string().email(),
shipping: z.object({
address: z.string(),
city: z.string(),
postalCode: z.string(),
country: z.string(),
}).optional(),
}),
status: z.enum(["pending", "processing", "shipped", "delivered"]),
}),
createdAt: z.string().datetime(),
notes: z.string().optional(),
});
const nestedTask = schemaTask({
id: "nested-schema-task",
schema: nestedSchema,
run: async (payload, { ctx }) => {
// Deep property access with full type safety
const orderId = payload.order.orderId;
const firstItem = payload.order.items[0];
const quantity = firstItem?.quantity ?? 0;
const email = payload.order.customer.email;
const city = payload.order.customer.shipping?.city;
const status = payload.order.status;
// Status is properly typed as enum
const isShipped: boolean = status === "shipped" || status === "delivered";
return {
orderId,
customerEmail: email,
itemCount: payload.order.items.length,
isShipped,
shippingCity: city ?? "N/A",
};
},
});
// Test 5: Trigger type safety
async function testTriggerTypes() {
// Valid trigger calls - should compile
const handle1 = await zodTask.trigger({
userId: "123",
userName: "John Doe",
userEmail: "john@example.com",
isActive: true,
score: 95.5,
tags: ["premium", "verified"],
metadata: { source: "web" },
});
// The following would cause TypeScript errors if uncommented:
/*
const handle2 = await zodTask.trigger({
userId: 123, // Error: Type 'number' is not assignable to type 'string'
userName: "Jane",
userEmail: "jane@example.com",
isActive: "yes", // Error: Type 'string' is not assignable to type 'boolean'
score: "high", // Error: Type 'string' is not assignable to type 'number'
tags: "single-tag", // Error: Type 'string' is not assignable to type 'string[]'
});
const handle3 = await zodTask.trigger({
// Error: Missing required properties
userId: "456",
userName: "Bob",
});
*/
// triggerAndWait with result handling
const result = await zodTask.triggerAndWait({
userId: "789",
userName: "Alice Smith",
userEmail: "alice@example.com",
isActive: false,
score: 88,
tags: ["new"],
});
if (result.ok) {
// Output is properly typed
const processedId: string = result.output.processedUserId;
const processedName: string = result.output.processedUserName;
const tagCount: number = result.output.tagCount;
}
// Using unwrap
try {
const output = await zodTask.triggerAndWait({
userId: "999",
userName: "Eve",
userEmail: "eve@example.com",
isActive: true,
score: 100,
tags: ["admin", "super"],
}).unwrap();
// Direct access to typed output
console.log(output.processedUserId);
console.log(output.processedUserName);
console.log(output.tagCount);
} catch (error) {
console.error("Task failed:", error);
}
}
// Test 6: Batch operations type safety
async function testBatchTypes() {
// Batch trigger
const batchHandle = await zodTask.batchTrigger([
{
payload: {
userId: "b1",
userName: "Batch User 1",
userEmail: "batch1@example.com",
isActive: true,
score: 75,
tags: ["batch"],
},
},
{
payload: {
userId: "b2",
userName: "Batch User 2",
userEmail: "batch2@example.com",
isActive: false,
score: 82,
tags: ["batch", "test"],
},
},
]);
// Batch trigger and wait
const batchResult = await zodTask.batchTriggerAndWait([
{
payload: {
userId: "b3",
userName: "Batch User 3",
userEmail: "batch3@example.com",
isActive: true,
score: 90,
tags: [],
},
},
]);
// Process batch results with type safety
for (const run of batchResult.runs) {
if (run.ok) {
const userId: string = run.output.processedUserId;
const userName: string = run.output.processedUserName;
const tagCount: number = run.output.tagCount;
}
}
}
// Test 7: Verify satisfies works for JSON Schema
const schemaWithSatisfies = {
type: "object",
properties: {
foo: { type: "string" },
},
required: ["foo"],
} satisfies JSONSchema;
const taskWithSatisfies = task({
id: "task-with-satisfies",
payloadSchema: schemaWithSatisfies,
run: async (payload) => {
return { foo: payload.foo };
},
});
// If this file compiles without errors, our implementation is working correctly!
console.log("Type tests completed successfully!");
+1 -1
View File
@@ -37,7 +37,7 @@
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"uploadthing": "^7.1.0",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@next/bundle-analyzer": "^15.0.2",
+1 -1
View File
@@ -10,7 +10,7 @@
"dependencies": {
"@trigger.dev/sdk": "workspace:*",
"@trigger.dev/python": "workspace:*",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@trigger.dev/build": "workspace:*",
+1 -1
View File
@@ -8,7 +8,7 @@
},
"dependencies": {
"@trigger.dev/sdk": "workspace:*",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@trigger.dev/build": "workspace:*",
+1 -1
View File
@@ -62,7 +62,7 @@
"yt-dlp-wrap": "^2.3.12",
"yup": "^1.4.0",
"zip-node-addon": "^0.0.11",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@opentelemetry/api": "^1.8.0",