Skip to main content

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.

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

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

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

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

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'
      }
    }
  }
)
HTTP trigger handlers receive an ApiRequest and must return an ApiResponse with status_code, optional body, and optional headers.

Event Triggers

The Engine provides system event triggers:

Functions Available Event

Triggered when function registry changes (functions added/removed):
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:
// 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:
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:
// 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:
// Source: packages/node/iii/src/types.ts:46
type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'all'

Schedule Triggers

Schedule triggers are handled by the Engine’s scheduler. Configuration format depends on Engine implementation (cron expressions, intervals, etc.).
Example pattern:
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

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

1

Registration

Worker sends RegisterTrigger message to Engine with type, function_id, and config
2

Handler Invocation

Engine invokes the trigger type’s registerTrigger handler (if custom type)
3

Event Source Setup

Handler sets up event listener, webhook server, scheduler, etc.
4

Active

Trigger invokes function when events occur
5

Unregistration

Worker calls trigger.unregister() or disconnects, Handler’s unregisterTrigger is called

Multi-Language Support

const trigger = iii.registerTrigger({
  type: 'http',
  function_id: 'api::hello',
  config: { api_path: '/hello', http_method: 'GET' }
})

trigger.unregister()

Next Steps

Functions

Learn about function registration and invocation

Channels

Stream data between functions

Build docs developers (and LLMs) love