Skip to content
View as Markdown

Setting up an Event-based Trigger ​

Event-based triggers allow you to automatically run actions when specific database events occur. In this tutorial, we'll create an executor that sends a Slack notification when a Project's status changes to "completed".

Tutorial Steps ​

To create an event-based trigger, you'll need to:

  1. Configure the Executor service
  2. Create the executor with a record updated trigger
  3. Enable publishEvents feature on the Project table
  4. Deploy the changes
  5. Verify the trigger

1. Configure the Executor Service ​

Update your tailor.config.ts to include the executor service:

typescript
import { defineConfig } from "@tailor-platform/sdk";

export default defineConfig({
  name: "project-management",
  db: {
    "main-db": {
      files: ["db/**/*.ts"],
    },
  },
  executor: {
    files: ["executor/**/*.ts"],
  },
});

This configures the SDK to load executor definitions from the executor/ directory.

2. Create the Executor with Record Updated Trigger ​

Create a new file executor/notify-project-completion.ts:

typescript
import { createExecutor, recordUpdatedTrigger } from "@tailor-platform/sdk";
import { project } from "../db/project";
import { getDB } from "../generated/tailordb";

export default createExecutor({
  name: "notify-project-completion",
  description: "Send Slack notification when project is completed",
  trigger: recordUpdatedTrigger({
    type: project,
    condition: ({ newRecord, oldRecord }) =>
      newRecord.status === "completed" && oldRecord.status !== "completed",
  }),
  operation: {
    kind: "webhook",
    url: () => "https://hooks.slack.com/services/YOUR_WEBHOOK_URL",
    headers: {
      "Content-Type": "application/json",
    },
    requestBody: async ({ newRecord }) => {
      const db = getDB("main-db");

      // Count tasks for summary
      const taskStats = await db
        .selectFrom("Task")
        .select(({ fn }) => [
          fn.count<number>("id").as("total"),
          fn.countAll<number>().filterWhere("status", "=", "DONE").as("completed"),
        ])
        .where("projectId", "=", newRecord.id)
        .executeTakeFirst();

      return {
        text: `🎉 Project Completed: ${newRecord.name}`,
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: `*Project:* ${newRecord.name}\n*Status:* ${newRecord.status}\n*Tasks:* ${taskStats?.completed}/${taskStats?.total} completed`,
            },
          },
        ],
      };
    },
  },
});

Key Components:

  1. Trigger: recordUpdatedTrigger() fires when a Project record is updated

    • type: project: Specifies which table to monitor
    • condition: Only triggers when status changes TO "completed" (not already completed)
  2. Operation: webhook sends HTTP POST request to Slack

    • url: Your Slack webhook URL (replace with actual URL from Slack)
    • requestBody: Constructs the Slack message with project details and task statistics
  3. Database Query: Uses Kysely to query task completion statistics before sending the notification

3. Enable publishEvents Feature ​

To ensure the executor triggers when Project records are updated, enable publishEvents in your Project table definition.

Update your db/project.ts file:

typescript
import { db } from "@tailor-platform/sdk";

export const project = db
  .table("Project", {
    name: db.string().description("Project name"),
    description: db.string({ optional: true }).description("Project description"),
    status: db
      .enum(["planning", "active", "completed", "archived"])
      .description("Current project status"),
    startDate: db.string({ optional: true }).description("Project start date"),
    endDate: db.string({ optional: true }).description("Project end date"),
    createdAt: db.string().description("Creation timestamp"),
    updatedAt: db.string().description("Last update timestamp"),
  })
  .features({
    publishEvents: true,
  });

The .features({ publishEvents: true }) configuration enables the platform to publish events when Project records are created, updated, or deleted. Without this feature enabled, the executor trigger will not fire.

4. Deploy the Changes ​

Before deploying, make sure you have:

  1. Created a Slack webhook URL (see Slack Incoming Webhooks)
  2. Replaced YOUR_WEBHOOK_URL in the executor code with your actual webhook URL

Generate Kysely types for type-safe database access:

bash
npm run generate

This generates TypeScript types and the getDB() helper in the generated/ directory.

Deploy your application:

bash
npm run deploy -- --workspace-id <your-workspace-id>

The SDK will deploy both the updated Project table with publishEvents enabled and the new executor.

5. Verify the Trigger ​

Open the Console, navigate to your workspace, and open the executors to view the created executor.

Test the Executor:

  1. Create a test project: In the GraphQL Playground, create a project with some tasks:
graphql
mutation {
  createProject(
    input: {
      name: "Website Redesign"
      description: "Complete redesign of company website"
      status: "active"
      createdAt: "2026-02-09T10:00:00Z"
      updatedAt: "2026-02-09T10:00:00Z"
    }
  ) {
    id
  }
}
  1. Create some tasks for the project (use the project ID from above):
graphql
mutation {
  createTask(
    input: {
      title: "Design homepage"
      projectId: "<project-id>"
      status: "DONE"
      createdAt: "2026-02-09T10:00:00Z"
      updatedAt: "2026-02-09T10:00:00Z"
    }
  ) {
    id
  }
}
  1. Update the project status to "completed":
graphql
mutation {
  updateProject(
    id: "<project-id>"
    input: { status: "completed", updatedAt: "2026-02-09T11:00:00Z" }
  ) {
    id
    status
  }
}
  1. Check Slack: You should receive a notification in your Slack channel with the project name and task completion statistics.

  2. View executor logs: In the Console, open the executor's jobs to see its execution history. Each job lists its execution attempts. The Tailor Platform automatically retries failed webhook calls (non-2xx status codes), except for configuration or script errors.

Troubleshooting:

  • No notification received: Verify your Slack webhook URL is correct
  • Executor not triggering: Ensure publishEvents: true is set in .features() on the Project table
  • Error in logs: Check the executor logs in the Console for detailed error messages

Multi-Event Triggers ​

Instead of creating separate executors for each event type, you can use recordTrigger() to handle multiple events in a single executor. This is useful when the response logic is similar across events.

Update executor/notify-project-completion.ts to also handle project creation:

typescript
import { createExecutor, recordTrigger } from "@tailor-platform/sdk";
import { project } from "../db/project";

export default createExecutor({
  name: "notify-project-changes",
  description: "Send Slack notification on project create or status change to completed",
  trigger: recordTrigger({
    type: project,
    events: ["created", "updated"],
  }),
  operation: {
    kind: "webhook",
    url: () => "https://hooks.slack.com/services/YOUR_WEBHOOK_URL",
    headers: {
      "Content-Type": "application/json",
    },
    requestBody: async ({ event, newRecord, oldRecord }) => {
      if (event === "created") {
        return {
          text: `📋 New Project Created: ${newRecord.name}`,
        };
      }

      // event === "updated"
      if (newRecord.status === "completed" && oldRecord?.status !== "completed") {
        return {
          text: `🎉 Project Completed: ${newRecord.name}`,
        };
      }

      return null; // No notification for other updates
    },
  },
});

Key differences from single-event triggers:

Single-event (recordUpdatedTrigger)Multi-event (recordTrigger)
ImportrecordUpdatedTriggerrecordTrigger
EventsFixed to one eventPass events: [...] array
ArgsAlways has oldRecord + newRecordUse args.event to narrow the type
Use caseSimple, focused handlersShared logic across create/update/delete

The event field on args is typed to match the events array you pass, so TypeScript will narrow correctly in if (event === "created") branches.

Next Steps ​

Learn more about executors: