Skip to content
View as Markdown

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
typescript
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 created
  • recordUpdatedTrigger(): Fires when a record is updated
  • recordDeletedTrigger(): Fires when a record is deleted

Each trigger can include an optional filter function:

typescript
recordUpdatedTrigger({
  type: order,
  condition: ({ newRecord, oldRecord }) =>
    newRecord.status === "completed" && oldRecord.status !== "completed",
});

Schedule Trigger

Fires on a cron schedule:

typescript
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:

typescript
type WebhookRequest = {
  body: WebhookPayload;
  headers: Record<string, string>;
};

incomingWebhookTrigger<WebhookRequest>();

You can customize the HTTP response returned to the webhook caller:

typescript
// 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:

typescript
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 created
  • idpUserUpdatedTrigger(): Fires when an IdP user is updated
  • idpUserDeletedTrigger(): Fires when an IdP user is deleted
typescript
idpUserCreatedTrigger();

When the project defines multiple IdPs, pass idp to target a specific one. The name is type-narrowed via the generated IdpName type:

typescript
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 issued
  • authAccessTokenRefreshedTrigger(): Fires when an access token is refreshed
  • authAccessTokenRevokedTrigger(): Fires when an access token is revoked
typescript
authAccessTokenIssuedTrigger();

Workflow Execution Triggers

Fire when a workflow execution changes state. Use the single-event helpers or workflowExecutionTrigger() for multiple events:

typescript
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()

typescript
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()

typescript
idpUserTrigger({ events: ["created", "deleted"] });

In multi-IdP projects, add idp to target a specific IdP:

typescript
idpUserTrigger({ events: ["created", "deleted"], idp: "my-idp" });

authAccessTokenTrigger()

typescript
authAccessTokenTrigger({ events: ["issued", "revoked"] });

workflowExecutionTrigger() and workflowJobExecutionTrigger()

typescript
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:

typescript
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.

typescript
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:

typescript
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:

typescript
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.

typescript
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:

typescript
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:

typescript
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

typescript
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

typescript
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

typescript
interface RecordDeletedContext<T> {
  event: "deleted";
  rawEvent: "tailordb.type_record.deleted";
  workspaceId: string;
  appNamespace: string;
  typeName: string;
  oldRecord: T; // The deleted record
}

Usage Example:

typescript
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:

typescript
interface ScheduleContext {
  scheduledTime: string; // ISO 8601 timestamp
}

Incoming Webhook Payload

Webhook triggers receive HTTP request data:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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.

typescript
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:

typescript
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.