> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/iii-hq/sdk/llms.txt
> Use this file to discover all available pages before exploring further.

# Triggers

> Trigger system, trigger types (HTTP, events, schedules), and custom triggers

## Overview

Triggers are event sources that automatically invoke functions when specific conditions occur. The III Engine provides built-in trigger types (HTTP endpoints, scheduled jobs, system events) and supports custom trigger types implemented by workers.

## Trigger Registration

Register a trigger to connect an event source to a function:

```typescript theme={null}
const trigger = iii.registerTrigger({
  type: 'http',                    // Trigger type
  function_id: 'api::users::list', // Function to invoke
  config: {                        // Type-specific configuration
    api_path: '/users',
    http_method: 'GET'
  }
})

// Later: trigger.unregister()
```

**Trigger registration message:**

```typescript theme={null}
// Source: packages/node/iii/src/iii-types.ts:41-47
type RegisterTriggerMessage = {
  message_type: MessageType.RegisterTrigger
  id: string              // Auto-generated UUID
  type: string            // Trigger type identifier
  function_id: string     // Target function
  config: unknown         // Type-specific configuration
}
```

### Registration Flow

```mermaid theme={null}
sequenceDiagram
    participant W as Worker
    participant E as Engine
    participant TH as Trigger Handler
    
    W->>E: RegisterTrigger {type, function_id, config}
    E->>TH: registerTrigger(config)
    TH-->>TH: Set up event source
    TH->>E: Success/Error
    E->>W: TriggerRegistrationResult
```

**Implementation:**

```typescript theme={null}
// Source: packages/node/iii/src/iii.ts:166-186
registerTrigger = (trigger: Omit<RegisterTriggerMessage, 'message_type' | 'id'>): Trigger => {
  const id = crypto.randomUUID()
  const fullTrigger: RegisterTriggerMessage = {
    ...trigger,
    id,
    message_type: MessageType.RegisterTrigger
  }
  this.sendMessage(MessageType.RegisterTrigger, fullTrigger, true)
  this.triggers.set(id, fullTrigger)
  
  return {
    unregister: () => {
      this.sendMessage(MessageType.UnregisterTrigger, {
        id,
        message_type: MessageType.UnregisterTrigger,
        type: fullTrigger.type
      })
      this.triggers.delete(id)
    }
  }
}
```

## HTTP Triggers

HTTP triggers expose functions as REST API endpoints:

### Basic HTTP Endpoint

```typescript theme={null}
import type { ApiRequest, ApiResponse } from 'iii-sdk'

const fn = iii.registerFunction(
  { id: 'api::products::list' },
  async (req: ApiRequest): Promise<ApiResponse> => {
    const products = await db.products.findMany()
    
    return {
      status_code: 200,
      body: { products },
      headers: { 'Content-Type': 'application/json' }
    }
  }
)

iii.registerTrigger({
  type: 'http',
  function_id: fn.id,
  config: {
    api_path: '/products',
    http_method: 'GET',
    description: 'List all products'
  }
})
```

**HTTP request structure:**

```typescript theme={null}
// Source: packages/node/iii/src/types.ts:222-230
type ApiRequest<TBody = unknown> = {
  path_params: Record<string, string>           // Route parameters
  query_params: Record<string, string | string[]> // Query string
  body: TBody                                   // Parsed JSON body
  headers: Record<string, string | string[]>    // Request headers
  method: string                                // HTTP method
  request_body: ChannelReader                   // Raw body stream
}
```

### Path Parameters

Capture dynamic segments from URL:

```typescript theme={null}
iii.registerFunction(
  { id: 'api::products::get' },
  async (req: ApiRequest): Promise<ApiResponse> => {
    const { id } = req.path_params
    const product = await db.products.findById(id)
    
    if (!product) {
      return { status_code: 404, body: { error: 'Product not found' } }
    }
    
    return { status_code: 200, body: product }
  }
)

iii.registerTrigger({
  type: 'http',
  function_id: 'api::products::get',
  config: {
    api_path: '/products/:id',  // :id becomes path parameter
    http_method: 'GET'
  }
})
```

**Example from tests:**

```typescript theme={null}
// Source: packages/node/iii/tests/api-triggers.test.ts:76-103
const fn = iii.registerFunction(
  { id: 'test.api.getById' },
  async (req: HttpRequest): Promise<ApiResponse> => ({
    status_code: 200,
    body: { id: req.path_params?.id }
  })
)

const trigger = iii.registerTrigger({
  type: 'http',
  function_id: fn.id,
  config: {
    api_path: 'test/items/:id',
    http_method: 'GET'
  }
})

const response = await httpRequest('GET', '/test/items/abc123')
expect(response.data).toEqual({ id: 'abc123' })
```

### Query Parameters

```typescript theme={null}
iii.registerFunction(
  { id: 'api::search' },
  async (req: ApiRequest): Promise<ApiResponse> => {
    const q = Array.isArray(req.query_params.q) 
      ? req.query_params.q[0] 
      : req.query_params.q
    const limit = parseInt(req.query_params.limit as string) || 10
    
    const results = await db.search(q, limit)
    return { status_code: 200, body: { results, query: q } }
  }
)

iii.registerTrigger({
  type: 'http',
  function_id: 'api::search',
  config: {
    api_path: '/search',
    http_method: 'GET'
  }
})

// GET /search?q=widget&limit=20
```

### Request Body

Parse JSON body automatically:

```typescript theme={null}
type CreateUserRequest = {
  email: string
  name: string
  role: 'admin' | 'user'
}

iii.registerFunction(
  { id: 'api::users::create' },
  async (req: ApiRequest<CreateUserRequest>): Promise<ApiResponse> => {
    const { email, name, role } = req.body
    
    // Validation
    if (!email || !name) {
      return { 
        status_code: 400, 
        body: { error: 'Email and name are required' } 
      }
    }
    
    const user = await db.users.create({ email, name, role })
    return { status_code: 201, body: user }
  }
)

iii.registerTrigger({
  type: 'http',
  function_id: 'api::users::create',
  config: {
    api_path: '/users',
    http_method: 'POST'
  }
})
```

### Custom Response Headers

```typescript theme={null}
const fn = iii.registerFunction(
  { id: 'api::download' },
  async (req: ApiRequest): Promise<ApiResponse> => {
    const file = await generateReport()
    
    return {
      status_code: 200,
      body: file,
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename="report.pdf"',
        'Cache-Control': 'no-cache'
      }
    }
  }
)
```

<Note>
  HTTP trigger handlers receive an `ApiRequest` and must return an `ApiResponse` with `status_code`, optional `body`, and optional `headers`.
</Note>

## Event Triggers

The Engine provides system event triggers:

### Functions Available Event

Triggered when function registry changes (functions added/removed):

```typescript theme={null}
iii.onFunctionsAvailable((functions) => {
  console.log(`${functions.length} functions available`)
  
  // Check if required services are online
  const requiredServices = ['payments::charge', 'inventory::reserve']
  const available = requiredServices.every(id => 
    functions.some(f => f.function_id === id)
  )
  
  if (available) {
    console.log('All required services are online')
  }
})
```

**Implementation detail:**

```typescript theme={null}
// Source: packages/node/iii/src/iii-constants.ts:13-16
export const EngineTriggers = {
  FUNCTIONS_AVAILABLE: 'engine::functions-available',
  LOG: 'log'
} as const
```

The SDK creates an internal function and trigger automatically. See `packages/node/iii/src/iii.ts:393-428`.

### Log Events

Receive OpenTelemetry log events from the Engine:

```typescript theme={null}
iii.onLog((log) => {
  console.log(`[${log.severity_text}] ${log.body}`)
  console.log('  Service:', log.service_name)
  console.log('  Trace ID:', log.trace_id)
  console.log('  Attributes:', log.attributes)
}, { level: 'warn' })  // Only WARN and above
```

**OtelLogEvent structure:**

```typescript theme={null}
// Source: packages/node/iii/src/types.ts:18-43
type OtelLogEvent = {
  timestamp_unix_nano: number
  observed_timestamp_unix_nano: number
  severity_number: number              // OTEL severity (1-24)
  severity_text: string                // "INFO", "WARN", "ERROR", etc.
  body: string                         // Log message
  attributes: Record<string, unknown>  // Structured attributes
  trace_id?: string                    // For correlation
  span_id?: string
  resource: Record<string, string>     // Resource attributes
  service_name: string                 // Emitting service
  instrumentation_scope_name?: string
  instrumentation_scope_version?: string
}
```

**Severity levels:**

```typescript theme={null}
// Source: packages/node/iii/src/types.ts:46
type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'all'
```

## Schedule Triggers

<Note>
  Schedule triggers are handled by the Engine's scheduler. Configuration format depends on Engine implementation (cron expressions, intervals, etc.).
</Note>

Example pattern:

```typescript theme={null}
iii.registerFunction(
  { id: 'jobs::cleanup' },
  async () => {
    await db.sessions.deleteExpired()
    return { deleted: 42 }
  }
)

iii.registerTrigger({
  type: 'schedule',
  function_id: 'jobs::cleanup',
  config: {
    cron: '0 * * * *'  // Every hour
  }
})
```

## Custom Trigger Types

Workers can implement custom trigger types for any event source:

### Implementing a Trigger Handler

```typescript theme={null}
import type { TriggerHandler } from 'iii-sdk'

type WebhookTriggerConfig = {
  webhook_url: string
  secret: string
}

const webhookHandler: TriggerHandler<WebhookTriggerConfig> = {
  async registerTrigger(config) {
    const { id, function_id, config: { webhook_url, secret } } = config
    
    // Set up webhook listener
    const listener = await createWebhookListener(webhook_url, secret)
    
    listener.on('event', async (data) => {
      // Invoke the registered function
      await iii.call(function_id, data)
    })
    
    // Store listener for cleanup
    webhookListeners.set(id, listener)
  },
  
  async unregisterTrigger(config) {
    const listener = webhookListeners.get(config.id)
    if (listener) {
      await listener.close()
      webhookListeners.delete(config.id)
    }
  }
}
```

**TriggerHandler interface:**

```typescript theme={null}
// Source: packages/node/iii/src/triggers.ts:1-11
type TriggerConfig<TConfig> = {
  id: string
  function_id: string
  config: TConfig
}

type TriggerHandler<TConfig> = {
  registerTrigger(config: TriggerConfig<TConfig>): Promise<void>
  unregisterTrigger(config: TriggerConfig<TConfig>): Promise<void>
}
```

### Registering Trigger Types

Register your custom trigger type with the Engine:

```typescript theme={null}
iii.registerTriggerType(
  {
    id: 'webhook',
    description: 'External webhook events'
  },
  webhookHandler
)

// Now any worker can use this trigger type
iii.registerTrigger({
  type: 'webhook',
  function_id: 'notifications::process',
  config: {
    webhook_url: 'https://example.com/hook',
    secret: process.env.WEBHOOK_SECRET
  }
})
```

**Implementation:**

```typescript theme={null}
// Source: packages/node/iii/src/iii.ts:146-155
registerTriggerType = <TConfig>(
  triggerType: Omit<RegisterTriggerTypeMessage, 'message_type'>,
  handler: TriggerHandler<TConfig>
): void => {
  this.sendMessage(MessageType.RegisterTriggerType, triggerType, true)
  this.triggerTypes.set(triggerType.id, {
    message: { ...triggerType, message_type: MessageType.RegisterTriggerType },
    handler
  })
}
```

### Trigger Registration Result

The Engine sends confirmation after trigger registration:

```typescript theme={null}
// Source: packages/node/iii/src/iii-types.ts:32-39
type TriggerRegistrationResultMessage = {
  message_type: MessageType.TriggerRegistrationResult
  id: string
  type: string
  function_id: string
  result?: unknown
  error?: unknown  // { code: string, message: string }
}
```

Possible error codes:

* `trigger_type_not_found` - Trigger type not registered
* `trigger_registration_failed` - Handler threw exception

## Real-World Examples

### API Helper Hook

Simplify HTTP trigger registration:

```typescript theme={null}
// Source: packages/node/iii-example/src/hooks.ts:1-30
import { type ApiRequest, type ApiResponse, getContext } from 'iii-sdk'
import { iii } from './iii'

export const useApi = <TBody = any>(
  config: {
    api_path: string
    http_method: string
    description?: string
    metadata?: Record<string, unknown>
  },
  handler: (req: ApiRequest<TBody>, context: Context) => Promise<ApiResponse>
) => {
  const function_id = `api::${config.http_method.toLowerCase()}::${config.api_path}`
  
  iii.registerFunction(
    { id: function_id, metadata: config.metadata },
    req => handler(req, getContext())
  )
  
  iii.registerTrigger({
    type: 'http',
    function_id,
    config: {
      api_path: config.api_path,
      http_method: config.http_method,
      description: config.description,
      metadata: config.metadata
    }
  })
}
```

**Usage:**

```typescript theme={null}
// Source: packages/node/iii-example/src/index.ts:7-36
useApi(
  {
    api_path: '/todo',
    http_method: 'POST',
    description: 'Create a new todo',
    metadata: { tags: ['todo'] }
  },
  async (req, ctx) => {
    ctx.logger.info('Creating new todo', { body: req.body })
    
    const { description, dueDate } = req.body
    const todoId = `todo-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
    
    if (!description) {
      return { status_code: 400, body: { error: 'Description is required' } }
    }
    
    const newTodo: Todo = {
      id: todoId,
      description,
      groupId: 'inbox',
      createdAt: new Date().toISOString(),
      dueDate,
      completedAt: null
    }
    const todo = await streams.set<Todo>('todo', 'inbox', todoId, newTodo)
    
    return { status_code: 201, body: todo }
  }
)
```

### Database Change Trigger

Custom trigger for database changes:

```typescript theme={null}
type DbChangeTriggerConfig = {
  table: string
  operation: 'insert' | 'update' | 'delete'
}

const dbChangeHandler: TriggerHandler<DbChangeTriggerConfig> = {
  async registerTrigger(config) {
    const { id, function_id, config: { table, operation } } = config
    
    // Set up database listener (e.g., PostgreSQL LISTEN/NOTIFY)
    const listener = await db.listen(`${table}_${operation}`)
    
    listener.on('notification', async (payload) => {
      await iii.callVoid(function_id, {
        table,
        operation,
        data: payload
      })
    })
    
    dbListeners.set(id, listener)
  },
  
  async unregisterTrigger(config) {
    const listener = dbListeners.get(config.id)
    await listener?.unlisten()
    dbListeners.delete(config.id)
  }
}

iii.registerTriggerType(
  { id: 'db-change', description: 'Database change events' },
  dbChangeHandler
)

iii.registerTrigger({
  type: 'db-change',
  function_id: 'cache::invalidate',
  config: { table: 'products', operation: 'update' }
})
```

## Trigger Lifecycle

<Steps>
  <Step title="Registration">
    Worker sends `RegisterTrigger` message to Engine with type, function\_id, and config
  </Step>

  <Step title="Handler Invocation">
    Engine invokes the trigger type's `registerTrigger` handler (if custom type)
  </Step>

  <Step title="Event Source Setup">
    Handler sets up event listener, webhook server, scheduler, etc.
  </Step>

  <Step title="Active">
    Trigger invokes function when events occur
  </Step>

  <Step title="Unregistration">
    Worker calls `trigger.unregister()` or disconnects, Handler's `unregisterTrigger` is called
  </Step>
</Steps>

## Multi-Language Support

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const trigger = iii.registerTrigger({
      type: 'http',
      function_id: 'api::hello',
      config: { api_path: '/hello', http_method: 'GET' }
    })

    trigger.unregister()
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    trigger = iii.register_trigger(
        type='http',
        function_id='api::hello',
        config={'api_path': '/hello', 'http_method': 'GET'}
    )

    trigger.unregister()
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use iii::RegisterTriggerMessage;
    use serde_json::json;

    let trigger = iii.register_trigger(RegisterTriggerMessage {
        r#type: "http".into(),
        function_id: "api::hello".into(),
        config: json!({"api_path": "/hello", "http_method": "GET"}),
        ..Default::default()
    })?;

    trigger.unregister();
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Functions" icon="function" href="/concepts/functions">
    Learn about function registration and invocation
  </Card>

  <Card title="Channels" icon="arrows-left-right" href="/concepts/channels">
    Stream data between functions
  </Card>
</CardGroup>
