Executor
Executors are event-driven handlers that automatically trigger in response to data changes, schedules, or external events.
Overview
Executors provide:
- Automatic triggers on record changes (create, update, delete)
- Scheduled execution via cron expressions
- Incoming webhook handlers
- Post-resolver execution hooks
- Multiple operation types (functions, webhooks, GraphQL, workflows)
For the official Tailor Platform documentation, see Executor Guide.
Creating an Executor
Define executors in files matching glob patterns specified in tailor.config.ts.
Definition Rules:
- One executor per file: Each file must contain exactly one executor definition
- Export method: Must use
export default - Uniqueness: Executor names must be unique globally across your entire application
import { createExecutor, recordCreatedTrigger, t } from "@tailor-platform/sdk";
import { user } from "../tailordb/user";
export default createExecutor({
name: "user-welcome",
description: "Send welcome email to new users",
trigger: recordCreatedTrigger({
type: user,
condition: ({ newRecord }) => !!newRecord.email && newRecord.isActive,
}),
operation: {
kind: "function",
body: async ({ newRecord }) => {
// Send welcome email logic here
},
},
});Trigger Types
Record Triggers
Fire when records are created, updated, or deleted:
recordCreatedTrigger(): Fires when a new record is createdrecordUpdatedTrigger(): Fires when a record is updatedrecordDeletedTrigger(): Fires when a record is deleted
Each trigger can include an optional filter function:
recordUpdatedTrigger({
type: order,
condition: ({ newRecord, oldRecord }) =>
newRecord.status === "completed" && oldRecord.status !== "completed",
});Schedule Trigger
Fires on a cron schedule:
scheduleTrigger({ cron: "*/5 * * * *" }); // Every 5 minutes
scheduleTrigger({ cron: "0 9 * * 1" }); // Every Monday at 9am
scheduleTrigger({ cron: "0 0 1 * *" }); // First day of every month
scheduleTrigger({ cron: "0 * * * *", timezone: "Asia/Tokyo" });Incoming Webhook Trigger
Fires when an external webhook is received:
type WebhookRequest = {
body: WebhookPayload;
headers: Record<string, string>;
};
incomingWebhookTrigger<WebhookRequest>();You can customize the HTTP response returned to the webhook caller:
// Response body only (shorthand)
incomingWebhookTrigger<WebhookRequest>({
response: (args) => ({ challenge: args.body.challenge }),
});
// Response body with custom status code
incomingWebhookTrigger<WebhookRequest>({
response: {
body: (args) => ({ challenge: args.body.challenge }),
statusCode: 201,
},
});If body is set without statusCode, the platform uses 200. If neither is set, the platform returns 204.
Resolver Executed Trigger
Fires when a resolver is executed:
resolverExecutedTrigger({
resolver: createOrderResolver,
condition: ({ result, error }) => !error && result?.order?.id,
});IdP User Triggers
Fire when IdP users are created, updated, or deleted:
idpUserCreatedTrigger(): Fires when a new IdP user is createdidpUserUpdatedTrigger(): Fires when an IdP user is updatedidpUserDeletedTrigger(): Fires when an IdP user is deleted
idpUserCreatedTrigger();When the project defines multiple IdPs, pass idp to target a specific one. The name is type-narrowed via the generated IdpName type:
idpUserCreatedTrigger({ idp: "my-idp" });Omitting idp is allowed only when the project has exactly one IdP; otherwise deploy fails with an error listing the configured IdPs.
These triggers require the IdP to publish user lifecycle events. deploy enables publishEvents automatically on each IdP targeted by an idpUser trigger taking part in the same run, and turns it back off once no such trigger remains; set the value explicitly on defineIdp() to pin it. See IdP service - publishEvents.
Auth Access Token Triggers
Fire on auth access token lifecycle events:
authAccessTokenIssuedTrigger(): Fires when a new access token is issuedauthAccessTokenRefreshedTrigger(): Fires when an access token is refreshedauthAccessTokenRevokedTrigger(): Fires when an access token is revoked
authAccessTokenIssuedTrigger();Workflow Execution Triggers
Fire when a workflow execution changes state. Use the single-event helpers or workflowExecutionTrigger() for multiple events:
import { createExecutor, workflowExecutionTrigger } from "@tailor-platform/sdk";
import orderWorkflow from "../workflows/order";
export default createExecutor({
name: "order-workflow-finished",
trigger: workflowExecutionTrigger({
workflow: orderWorkflow,
events: ["completed", "retried"],
}),
operation: {
kind: "function",
body: async (args) => {
if (args.event === "completed" && !args.success) {
console.error(args.error);
}
},
},
});The available workflow events are started, completed, retried, resumed, wait_started, and wait_resolved. To observe job-level events, use workflowJobExecutionStartedTrigger(), workflowJobExecutionCompletedTrigger(), workflowJobExecutionWaitStartedTrigger(), workflowJobExecutionWaitResolvedTrigger(), or workflowJobExecutionTrigger().
completed events include success; when it is false, error contains the failure message. A job released from a wait point emits wait_resolved instead of completed.
These triggers require the workflow to publish execution events. deploy enables publishEvents automatically on each targeted workflow, and on every job of a workflow targeted by a workflowJobExecution* trigger, and turns it back off once no such trigger remains; set the value explicitly to pin it. See Workflow service - Execution Events.
Multi-Event Triggers
Handle multiple event types in a single executor using multi-event trigger factories. These accept an events array of short event names:
recordTrigger()
import { createExecutor, recordTrigger } from "@tailor-platform/sdk";
import { user } from "../tailordb/user";
export default createExecutor({
name: "user-changed",
trigger: recordTrigger({
type: user,
events: ["created", "updated"],
}),
operation: {
kind: "function",
body: async (args) => {
if (args.event === "created") {
console.log("User created:", args.newRecord.name);
}
if (args.event === "updated") {
console.log("User updated:", args.oldRecord.name, "->", args.newRecord.name);
}
},
},
});idpUserTrigger()
idpUserTrigger({ events: ["created", "deleted"] });In multi-IdP projects, add idp to target a specific IdP:
idpUserTrigger({ events: ["created", "deleted"], idp: "my-idp" });authAccessTokenTrigger()
authAccessTokenTrigger({ events: ["issued", "revoked"] });workflowExecutionTrigger() and workflowJobExecutionTrigger()
workflowExecutionTrigger({ workflow: orderWorkflow, events: ["started", "completed"] });
workflowJobExecutionTrigger({ workflow: orderWorkflow, events: ["started", "wait_resolved"] });The event field on args matches the short event name, enabling type narrowing. Record triggers use names such as "created", auth token triggers use "issued", and workflow triggers use "started", "completed", and "wait_resolved". The rawEvent field contains the full event type string (e.g., "tailordb.type_record.created").
Operation Types
Function Operation
Execute JavaScript/TypeScript functions:
createExecutor({
operation: {
kind: "function",
body: async ({ newRecord, env }) => {
console.log(`New record created in ${env.bar}:`, newRecord);
},
},
});Executor callbacks receive the trigger args, including env from defineConfig({ env }). function and jobFunction body args also include an invoker field: the principal running this function, or the machine user configured through the operation invoker option; null for anonymous calls. Other operation kinds (graphql, webhook, workflow) receive env through their callback args but do not pass invoker into those callbacks.
Job Function Operation
For long-running operations, use jobFunction which runs asynchronously and supports extended execution times. See Job Function Operation for details.
import { createExecutor, scheduleTrigger } from "@tailor-platform/sdk";
import { getDB } from "../generated/tailordb";
export default createExecutor({
name: "daily-report-generator",
description: "Generate daily reports",
trigger: scheduleTrigger({ cron: "0 0 * * *" }),
operation: {
kind: "jobFunction",
body: async () => {
const db = getDB("tailordb");
// Long-running report generation logic
const records = await db.selectFrom("Order").selectAll().execute();
// Process records...
},
},
});Webhook Operation
Call external webhooks with dynamic data:
createExecutor({
operation: {
kind: "webhook",
url: ({ typeName }) => `https://api.example.com/webhooks/${typeName}`,
headers: {
"Content-Type": "application/json",
"X-API-Key": { vault: "api-keys", key: "external-api" },
},
requestBody: ({ newRecord }) => ({
id: newRecord.id,
timestamp: new Date(),
data: newRecord,
}),
},
});GraphQL Operation
Execute GraphQL queries and mutations:
createExecutor({
operation: {
kind: "graphql",
appName: "my-app",
query: `
mutation UpdateUserStatus($id: ID!, $status: String!) {
updateUser(id: $id, input: { status: $status }) {
id
status
updatedAt
}
}
`,
variables: ({ newRecord }) => ({
id: newRecord.userId,
status: "active",
}),
},
});Workflow Operation
Trigger workflows from executors. See Workflow documentation for how to define workflows.
import { createExecutor, recordCreatedTrigger } from "@tailor-platform/sdk";
import { order } from "../tailordb/order";
import processOrderWorkflow from "../workflows/process-order";
export default createExecutor({
name: "order-processor",
description: "Process new orders via workflow",
trigger: recordCreatedTrigger({ type: order }),
operation: {
kind: "workflow",
workflow: processOrderWorkflow,
args: ({ newRecord }) => ({
orderId: newRecord.id,
customerId: newRecord.customerId,
}),
},
});You can also pass static arguments:
createExecutor({
operation: {
kind: "workflow",
workflow: dailyReportWorkflow,
args: { reportType: "summary" },
},
});args must match the workflow's main job input. It is required when that input is required and can be omitted when the workflow has no input. Static arguments can be JSON-compatible primitives, arrays, or plain objects; top-level null is not supported. An argument callback must return the same input type.
Authentication for Operations
GraphQL and Workflow operations can specify an invoker to execute with machine user credentials. Pass the machine user name as a plain string — it is type-narrowed to the names defined in your auth config:
import { createExecutor, scheduleTrigger } from "@tailor-platform/sdk";
export default createExecutor({
name: "scheduled-cleanup",
trigger: scheduleTrigger({ cron: "0 0 * * *" }),
operation: {
kind: "graphql",
query: `mutation { cleanupOldRecords { count } }`,
invoker: "batch-processor",
},
});Event Payloads
Each trigger type provides specific context data in the callback functions.
Record Event Payloads
Record triggers receive context based on the operation type:
Created Event
interface RecordCreatedContext<T> {
event: "created"; // Short event name for type narrowing
rawEvent: "tailordb.type_record.created"; // Full event type string
workspaceId: string; // Workspace identifier
appNamespace: string; // Application/namespace name
typeName: string; // TailorDB table name
newRecord: T; // The newly created record
}Updated Event
interface RecordUpdatedContext<T> {
event: "updated";
rawEvent: "tailordb.type_record.updated";
workspaceId: string;
appNamespace: string;
typeName: string;
oldRecord: T; // Previous record state
newRecord: T; // Current record state
}Deleted Event
interface RecordDeletedContext<T> {
event: "deleted";
rawEvent: "tailordb.type_record.deleted";
workspaceId: string;
appNamespace: string;
typeName: string;
oldRecord: T; // The deleted record
}Usage Example:
import { createExecutor, recordUpdatedTrigger, t } from "@tailor-platform/sdk";
import { order } from "../tailordb/order";
export default createExecutor({
name: "order-status-changed",
trigger: recordUpdatedTrigger({
type: order,
condition: ({ oldRecord, newRecord }) => oldRecord.status !== newRecord.status,
}),
operation: {
kind: "function",
body: async ({ oldRecord, newRecord, typeName }) => {
console.log(`${typeName} status changed:`);
console.log(` From: ${oldRecord.status}`);
console.log(` To: ${newRecord.status}`);
},
},
});Schedule Event Payload
Schedule triggers receive minimal context:
interface ScheduleContext {
scheduledTime: string; // ISO 8601 timestamp
}Incoming Webhook Payload
Webhook triggers receive HTTP request data:
interface WebhookContext<T = unknown> {
body: T; // Parsed request body
headers: Record<string, string>; // Request headers
method: "POST" | "GET" | "PUT" | "DELETE"; // HTTP method
rawBody: string; // Raw request body as string
}Usage Example:
import { createExecutor, incomingWebhookTrigger } from "@tailor-platform/sdk";
interface StripeWebhook {
type: string;
data: { object: { id: string; amount: number } };
}
export default createExecutor({
name: "stripe-webhook",
trigger: incomingWebhookTrigger<{
body: StripeWebhook;
headers: { "stripe-signature": string };
}>(),
operation: {
kind: "function",
body: async ({ body, headers }) => {
const signature = headers["stripe-signature"];
console.log(`Received ${body.type} event`);
// Process webhook...
},
},
});With custom response:
export default createExecutor({
name: "slack-challenge",
trigger: incomingWebhookTrigger<{
body: { challenge: string; type: string };
headers: Record<string, string>;
}>({
response: (args) => ({ challenge: args.body.challenge }),
}),
operation: {
kind: "function",
body: async ({ body }) => {
console.log(`Received ${body.type} event`);
},
},
});Resolver Executed Payload
Resolver triggers receive the resolver's result or error:
interface ResolverExecutedContext<TResult> {
workspaceId: string; // Workspace identifier
appNamespace: string; // Application/namespace name
resolverName: string; // Name of the executed resolver
result?: TResult; // Return value (on success)
error?: string; // Error message (on failure)
}Usage Example:
import { createExecutor, resolverExecutedTrigger } from "@tailor-platform/sdk";
import { createOrderResolver } from "../resolvers/create-order";
export default createExecutor({
name: "order-created-notification",
trigger: resolverExecutedTrigger({
resolver: createOrderResolver,
condition: ({ result, error }) => !error && !!result?.order,
}),
operation: {
kind: "function",
body: async ({ result, resolverName }) => {
console.log(`${resolverName} completed successfully`);
console.log(`Order ID: ${result.order.id}`);
},
},
});IdP User Event Payload
IdP user triggers receive user context:
interface IdpUserContext {
event: "created" | "updated" | "deleted"; // Short event name
rawEvent: string; // Full event type (e.g., "idp.user.created")
namespaceName: string; // IdP namespace name
userId: string; // The affected user ID
}Auth Access Token Event Payload
Auth access token triggers receive token context:
interface AuthAccessTokenContext {
event: "issued" | "refreshed" | "revoked"; // Short event name
rawEvent: string; // Full event type (e.g., "auth.access_token.issued")
namespaceName: string; // Auth namespace name
userId: string; // The user associated with the token
}Workflow Execution Event Payload
Workflow execution triggers receive execution context:
interface WorkflowExecutionContext {
workspaceId: string; // Workspace identifier
env: TailorEnv; // Environment variables from tailor.config.ts
actor: TailorActor | null; // Principal that triggered the workflow
workflowId: string; // Workflow resource ID
workflowName: string; // Workflow name
workflowExecutionId: string; // Workflow execution ID
event: "started" | "completed" | "retried" | "resumed" | "wait_started" | "wait_resolved";
rawEvent: string; // Full event type
}Completed events narrow on success. Failed executions include error; retried executions include retryCount and retryAfter.
body: async (args) => {
if (args.event === "completed" && !args.success) {
console.error(args.error);
}
};Workflow Job Execution Event Payload
Workflow job execution triggers include every WorkflowExecutionContext field above, plus job-specific fields:
interface WorkflowJobExecutionContext {
workflowJobExecutionId: string; // Job execution ID
jobFunctionName: string; // Name passed to createWorkflowJob
event: "started" | "completed" | "wait_started" | "wait_resolved";
rawEvent: string; // Full event type
}wait_started events include waitKey, plus JSON-serialized waitPayload when the wait point recorded one; wait_resolved events include waitKey.