Triggers

Triggers

Triggers define how and when your agents are activated. Beyond simple API calls, agents can respond to schedules, emails, file changes, and more.


Available Triggers

Trigger Description Use Case
ApiTrigger HTTP endpoint Chat interfaces, webhooks
Rate Schedule-based Cron jobs, periodic tasks
EmailTrigger Incoming emails Email automation
FileTrigger File system changes Document processing

API Trigger

The default trigger. Exposes your agent as an HTTP endpoint.

import { Agent, ApiTrigger } from "@shuttl-io/core";

export const chatAgent = new Agent({
    name: "ChatBot",
    systemPrompt: "You're a helpful assistant.",
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    triggers: [new ApiTrigger()],  // This is the default
});

Endpoints

When you run shuttl serve, these HTTP endpoints are created:

Method Path Description
POST /chat Send a message
POST /chat/:threadId Continue a conversation
GET /agents List available agents
GET /health Health check

!!! tip "Development vs Production" Use shuttl dev for interactive development with the TUI. Use shuttl serve to expose HTTP endpoints for production or integration testing.

Request Format

curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Hello!",
    "agentName": "ChatBot"
  }'

With attachments:

curl -X POST http://localhost:8080/chat \
  -H "Content-Type: multipart/form-data" \
  -F "message=Analyze this image" \
  -F "file=@photo.jpg"

Rate Trigger

Schedule agents to run at specific intervals or times.

Simple Intervals

import { Rate, StreamingOutcome } from "@shuttl-io/core";

// Every 5 minutes
triggers: [Rate.minutes(5).bindOutcome(new StreamingOutcome())]

// Every hour
triggers: [Rate.hours(1).bindOutcome(new SlackOutcome("#channel"))]

// Every day
triggers: [Rate.days(1).bindOutcome(new WebhookOutcome("https://..."))]

Available Interval Methods

Rate.milliseconds(500)  // Every 500ms
Rate.seconds(30)        // Every 30 seconds
Rate.minutes(15)        // Every 15 minutes
Rate.hours(6)           // Every 6 hours
Rate.days(1)            // Every day
Rate.weeks(1)           // Every week
Rate.months(1)          // Every month (30 days)

Cron Expressions

For precise scheduling, use cron syntax:

// 9 AM every weekday
Rate.cron("0 9 * * MON-FRI", "America/New_York")

// Every hour on the hour
Rate.cron("0 * * * *")

// First day of every month at midnight
Rate.cron("0 0 1 * *")

// Every 15 minutes during business hours
Rate.cron("*/15 9-17 * * MON-FRI", "America/New_York")

Cron Syntax Reference

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, SUN-SAT)
│ │ │ │ │
* * * * *

Custom Input on Trigger

Customize what the agent receives when triggered:

Rate.hours(1)
    .withOnTrigger({
        onTrigger: async () => {
            const stats = await fetchDailyStats();
            return [{
                typeName: "text",
                text: `Generate a report for: ${JSON.stringify(stats)}`,
            }];
        },
    })
    .bindOutcome(new SlackOutcome("#reports"))

Email Trigger

React to incoming emails.

import { EmailTrigger, EmailOutcome } from "@shuttl-io/core";

export const emailAgent = new Agent({
    name: "EmailResponder",
    systemPrompt: `You handle customer support emails.
    
    For each email:
    1. Understand the customer's issue
    2. Search the knowledge base for solutions
    3. Draft a helpful response`,
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    tools: [searchKnowledgeTool, draftEmailTool],
    triggers: [
        new EmailTrigger({
            address: "support@company.com",
            filters: {
                excludeSpam: true,
                subjectContains: ["help", "support", "issue"],
            },
        }),
    ],
    outcomes: [new EmailOutcome()],
});

Email Input Format

When triggered by an email, the agent receives:

{
    from: "customer@example.com",
    to: "support@company.com",
    subject: "Help with my order",
    body: "I placed an order yesterday but...",
    attachments: [/* any attached files */],
    receivedAt: "2025-01-03T10:30:00Z",
}

File Trigger

Watch for file system changes.

import { FileTrigger } from "@shuttl-io/core";

export const documentProcessor = new Agent({
    name: "DocProcessor",
    systemPrompt: "Process incoming documents and extract key information.",
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    tools: [extractTextTool, classifyDocTool, saveToDbTool],
    triggers: [
        new FileTrigger({
            path: "/incoming/documents",
            patterns: ["*.pdf", "*.docx"],
            events: ["created"],
        }),
    ],
});

Configuration Options

new FileTrigger({
    path: "/path/to/watch",      // Directory to monitor
    patterns: ["*.pdf"],          // Glob patterns to match
    events: ["created", "modified", "deleted"],  // Events to trigger on
    recursive: true,              // Watch subdirectories
    debounceMs: 1000,            // Wait for file to stabilize
})

Multiple Triggers

Agents can have multiple triggers:

export const flexibleAgent = new Agent({
    name: "FlexBot",
    systemPrompt: "You respond to various inputs.",
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    tools: [searchTool],
    triggers: [
        new ApiTrigger(),                    // HTTP endpoint
        Rate.hours(1),                       // Hourly check-in
        new EmailTrigger("bot@company.com"), // Email responses
    ],
});

Each trigger can have its own outcome:

triggers: [
    new ApiTrigger().bindOutcome(new StreamingOutcome()),
    Rate.hours(1).bindOutcome(new SlackOutcome("#updates")),
]

Binding Outcomes

Connect triggers to specific outcomes:

// Rate trigger → Slack
Rate.hours(1).bindOutcome(new SlackOutcome("#channel"))

// Rate trigger → Webhook
Rate.minutes(30).bindOutcome(new WebhookOutcome("https://api.example.com/hook"))

// Rate trigger → Multiple outcomes
Rate.days(1).bindOutcome(
    new CombinationOutcome([
        new SlackOutcome("#reports"),
        new WebhookOutcome("https://backup.example.com"),
    ])
)

Trigger Patterns

The Poller

Check for updates periodically:

const pollerAgent = new Agent({
    name: "DataPoller",
    systemPrompt: "Check for new data and process it.",
    model: Model.openAI("gpt-4o-mini", Secret.fromEnv("KEY")),
    tools: [checkForUpdatesTool, processUpdateTool],
    triggers: [
        Rate.minutes(5).withOnTrigger({
            onTrigger: async () => [{
                typeName: "text",
                text: "Check for new data and process any updates.",
            }],
        }),
    ],
});

The Reporter

Generate scheduled reports:

const reporterAgent = new Agent({
    name: "DailyReporter",
    systemPrompt: "Generate comprehensive daily reports.",
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    tools: [fetchMetricsTool, generateChartTool],
    triggers: [
        Rate.cron("0 9 * * MON-FRI", "America/New_York")
            .withOnTrigger({
                onTrigger: async () => {
                    const date = new Date().toISOString().split("T")[0];
                    return [{
                        typeName: "text",
                        text: `Generate the daily report for ${date}.`,
                    }];
                },
            })
            .bindOutcome(new SlackOutcome("#reports")),
    ],
});

The Hybrid

Respond to both API calls and schedules:

const hybridAgent = new Agent({
    name: "HybridBot",
    systemPrompt: `You perform analysis tasks.
    
    If invoked via API: Respond to the user's specific request.
    If invoked on schedule: Generate the standard daily summary.`,
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    tools: [analyzeTool, summarizeTool],
    triggers: [
        new ApiTrigger(),
        Rate.cron("0 18 * * *").bindOutcome(new SlackOutcome("#eod")),
    ],
});

Custom Triggers

Build your own triggers by implementing the ITrigger interface or extending BaseTrigger.

The ITrigger Interface

import { ITrigger, IOutcome, ITriggerInvoker } from "@shuttl-io/core";

interface ITrigger {
    /** Unique name of this trigger instance */
    name: string;
    
    /** The type of trigger (e.g., "webhook", "queue") */
    triggerType: string;
    
    /** Configuration for the trigger */
    triggerConfig: Record<string, unknown>;
    
    /** Optional bound outcome */
    outcome?: IOutcome;
    
    /** Activates the trigger and invokes the agent */
    activate(args: any, invoker: ITriggerInvoker): Promise<void>;
    
    /** Validates trigger arguments (optional) */
    validate?(args: any): Promise<Record<string, unknown>>;
    
    /** Binds an outcome to this trigger */
    bindOutcome(outcome: IOutcome): ITrigger;
    
    /** Sets the trigger name */
    withName(name: string): ITrigger;
}

Extending BaseTrigger (Recommended)

The BaseTrigger abstract class handles most boilerplate:

import { BaseTrigger, TriggerOutput, ITriggerInvoker, IOutcome } from "@shuttl-io/core";

class WebhookTrigger extends BaseTrigger {
    constructor(private webhookSecret: string) {
        super("webhook", { secret: webhookSecret });
    }
    
    // Parse incoming webhook payload into agent input
    async parseArgs(args: any): Promise<TriggerOutput> {
        const { event, data } = args;
        
        return {
            input: [{
                typeName: "text",
                text: `Webhook event: ${event}\nData: ${JSON.stringify(data)}`,
            }],
        };
    }
    
    // Optional: validate the webhook signature
    async validate(args: any): Promise<Record<string, unknown>> {
        const signature = args.headers?.["x-webhook-signature"];
        if (!this.verifySignature(signature, args.body)) {
            throw new Error("Invalid webhook signature");
        }
        return {};
    }
    
    private verifySignature(signature: string, body: any): boolean {
        // Your signature verification logic
        return true;
    }
}

TriggerOutput Format

The parseArgs method returns a TriggerOutput with the input for the agent:

interface TriggerOutput {
    input: InputContent[];
}

// InputContent can be text or file attachments
type InputContent = 
    | { typeName: "text"; text: string }
    | { typeName: "file"; file: string; fileData: FileAttachment };

Example: Queue Trigger

A trigger that processes messages from a queue:

class QueueTrigger extends BaseTrigger {
    constructor(private queueUrl: string) {
        super("queue", { queueUrl });
    }
    
    async parseArgs(args: any): Promise<TriggerOutput> {
        const message = args.message;
        const metadata = args.metadata || {};
        
        return {
            input: [{
                typeName: "text",
                text: `Process this queue message:
                
Message ID: ${metadata.messageId}
Received: ${metadata.timestamp}
Content: ${JSON.stringify(message)}`,
            }],
        };
    }
}

// Usage
export const queueAgent = new Agent({
    name: "QueueProcessor",
    systemPrompt: "Process incoming queue messages.",
    model: Model.openAI("gpt-4", Secret.fromEnv("KEY")),
    triggers: [
        new QueueTrigger("https://sqs.us-east-1.amazonaws.com/queue")
            .withName("order-queue")
            .bindOutcome(new SlackOutcome("#orders")),
    ],
});

Example: GitHub Webhook Trigger

class GitHubWebhookTrigger extends BaseTrigger {
    constructor(private webhookSecret: string) {
        super("github-webhook", { secret: webhookSecret });
    }
    
    async parseArgs(args: any): Promise<TriggerOutput> {
        const event = args.headers["x-github-event"];
        const payload = args.body;
        
        let prompt: string;
        
        switch (event) {
            case "pull_request":
                prompt = `Review this PR:
                    Title: ${payload.pull_request.title}
                    Author: ${payload.pull_request.user.login}
                    Description: ${payload.pull_request.body}
                    Changes: ${payload.pull_request.diff_url}`;
                break;
            case "issues":
                prompt = `Triage this issue:
                    Title: ${payload.issue.title}
                    Body: ${payload.issue.body}
                    Labels: ${payload.issue.labels.map(l => l.name).join(", ")}`;
                break;
            default:
                prompt = `GitHub ${event} event received: ${JSON.stringify(payload)}`;
        }
        
        return {
            input: [{ typeName: "text", text: prompt }],
        };
    }
    
    async validate(args: any): Promise<Record<string, unknown>> {
        const signature = args.headers["x-hub-signature-256"];
        if (!this.verifyGitHubSignature(signature, args.rawBody)) {
            throw new Error("Invalid GitHub webhook signature");
        }
        return { event: args.headers["x-github-event"] };
    }
}

The ITriggerInvoker

When activate is called, you receive an ITriggerInvoker that invokes the agent:

interface ITriggerInvoker {
    invoke(input: InputContent[]): Promise<IModelResponseStream>;
    defaultOutcome(response: IModelResponseStream): Promise<void>;
}

If you need full control over activation, override activate directly:

class CustomTrigger extends BaseTrigger {
    async parseArgs(args: any): Promise<TriggerOutput> {
        return { input: [{ typeName: "text", text: args.message }] };
    }
    
    // Override for custom activation logic
    async activate(args: any, invoker: ITriggerInvoker): Promise<void> {
        // Pre-processing
        console.log("Trigger activated:", this.name);
        
        const { input } = await this.parseArgs(args);
        const response = await invoker.invoke(input);
        
        // Route to outcome or default
        if (this.outcome) {
            await this.outcome.send(response);
        } else {
            await invoker.defaultOutcome(response);
        }
        
        // Post-processing
        console.log("Trigger completed:", this.name);
    }
}

Best Practices

1. Use Appropriate Intervals

Don't poll more often than necessary:

// ❌ Too aggressive
Rate.seconds(1)

// ✅ Reasonable for most use cases
Rate.minutes(5)

2. Handle Trigger Context

Use onTrigger to provide context:

Rate.hours(1).withOnTrigger({
    onTrigger: async () => {
        const context = await gatherContext();
        return [{
            typeName: "text",
            text: `Current context: ${JSON.stringify(context)}. 
                   Analyze and report any anomalies.`,
        }];
    },
})

3. Match Outcomes to Triggers

API triggers usually stream; scheduled triggers usually notify:

// API → Stream to user
new ApiTrigger().bindOutcome(new StreamingOutcome())

// Schedule → Post to Slack
Rate.days(1).bindOutcome(new SlackOutcome("#channel"))

4. Consider Timezones

Always specify timezone for cron expressions:

// ❌ Ambiguous
Rate.cron("0 9 * * *")

// ✅ Explicit
Rate.cron("0 9 * * *", "America/New_York")

Next Steps