Create a Custom Trigger Type
Register a custom trigger type so functions can be fired by events that iii does not handle natively.
Goal
Create a custom trigger type that fires functions in response to events iii doesn't handle out of the box — for example incoming webhooks, file-system changes, or third-party service callbacks.
Steps
1. Implement the trigger handler
A trigger handler is an object (Node) or class/trait (Python, Rust) with two callbacks:
registerTrigger— called when a function binds to your trigger type. Set up whatever listener or subscription is needed.unregisterTrigger— called when the binding is removed. Tear down the listener.
Both callbacks receive a TriggerConfig containing the trigger id, the bound function_id, and the caller-supplied config.
import { registerWorker, TriggerHandler } from 'iii-sdk'
import express from 'express'
type WebhookConfig = { path: string }
const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')
const app = express()
app.use(express.json())
const routes = new Map<string, string>()
const webhookHandler: TriggerHandler<WebhookConfig> = {
registerTrigger: async ({ function_id, config }) => {
routes.set(config.path, function_id)
app.post(config.path, async (req, res) => {
const result = await iii.trigger({ function_id, payload: req.body })
res.json(result)
})
},
unregisterTrigger: async ({ config }) => {
routes.delete(config.path)
},
}import os
from aiohttp import web
from iii import register_worker, TriggerConfig, TriggerHandler
iii_client = register_worker(os.environ.get("III_URL", "ws://localhost:49134"))
routes: dict[str, str] = {}
aiohttp_app = web.Application()
class WebhookHandler(TriggerHandler):
async def register_trigger(self, trigger: TriggerConfig) -> None:
path = trigger.config["path"]
function_id = trigger.function_id
routes[path] = function_id
async def handle(request: web.Request) -> web.Response:
body = await request.json()
result = iii_client.trigger({
"function_id": function_id,
"payload": body,
})
return web.json_response(result)
aiohttp_app.router.add_post(path, handle)
async def unregister_trigger(self, trigger: TriggerConfig) -> None:
routes.pop(trigger.config["path"], None)use async_trait::async_trait;
use iii_sdk::{TriggerConfig, TriggerHandler, IIIError};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
struct WebhookHandler {
routes: Arc<Mutex<HashMap<String, String>>>,
}
#[async_trait]
impl TriggerHandler for WebhookHandler {
async fn register_trigger(&self, config: TriggerConfig) -> Result<(), IIIError> {
let path = config.config["path"].as_str().unwrap_or("/").to_string();
self.routes.lock().await.insert(path, config.function_id);
Ok(())
}
async fn unregister_trigger(&self, config: TriggerConfig) -> Result<(), IIIError> {
let path = config.config["path"].as_str().unwrap_or("/");
self.routes.lock().await.remove(path);
Ok(())
}
}2. Register the trigger type
Call registerTriggerType with an id, a description, and the handler from above. The id is the string other workers will reference when they call registerTrigger.
iii.registerTriggerType(
{ id: 'webhook', description: 'External webhook trigger' },
webhookHandler,
)
app.listen(4000)import asyncio
iii_client.register_trigger_type({"id": "webhook", "description": "External webhook trigger"}, WebhookHandler())
async def main():
runner = web.AppRunner(aiohttp_app)
await runner.setup()
site = web.TCPSite(runner, "localhost", 4000)
await site.start()
await asyncio.Event().wait()
asyncio.run(main())use iii_sdk::{register_worker, InitOptions};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("III_URL").unwrap_or_else(|_| "ws://127.0.0.1:49134".to_string());
let iii = register_worker(&url, InitOptions::default());
let handler = WebhookHandler {
routes: Arc::new(Mutex::new(HashMap::new())),
};
iii.register_trigger_type("webhook", "External webhook trigger", handler);
tokio::signal::ctrl_c().await?;
Ok(())
}3. Bind functions to the custom trigger
From any worker — including one written in a different language — functions can now bind to the webhook trigger type with registerTrigger. The config you pass in config is forwarded directly to your handler's registerTrigger callback.
import { registerWorker, Logger } from 'iii-sdk'
const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')
iii.registerFunction({ id: 'github::push' }, async (payload) => {
const logger = new Logger()
logger.info('Push event received', { repo: payload.repository?.full_name })
return { ok: true }
})
iii.registerTrigger({
type: 'webhook',
function_id: 'github::push',
config: { path: '/hooks/github' },
})import os
from iii import register_worker, Logger
iii = register_worker(os.environ.get("III_URL", "ws://localhost:49134"))
def handle_push(payload):
logger = Logger()
logger.info("Push event received", {"repo": payload.get("repository", {}).get("full_name")})
return {"ok": True}
iii.register_function({"id": "github::push"}, handle_push)
iii.register_trigger({
"type": "webhook",
"function_id": "github::push",
"config": {"path": "/hooks/github"},
})use iii_sdk::{register_worker, InitOptions, Logger, RegisterFunctionMessage, RegisterTriggerInput};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("III_URL").unwrap_or_else(|_| "ws://127.0.0.1:49134".to_string());
let iii = register_worker(&url, InitOptions::default());
iii.register_function(
RegisterFunctionMessage {
id: "github::push".into(),
description: None,
request_format: None,
response_format: None,
metadata: None,
invocation: None,
},
|payload| async move {
let logger = Logger::new();
let repo = payload["repository"]["full_name"].as_str().unwrap_or("unknown");
logger.info("Push event received", Some(json!({ "repo": repo })));
Ok(json!({ "ok": true }))
},
);
iii.register_trigger(RegisterTriggerInput {
trigger_type: "webhook".into(),
function_id: "github::push".into(),
config: json!({ "path": "/hooks/github" }),
})?;
tokio::signal::ctrl_c().await?;
Ok(())
}4. Unregister the trigger type
When the worker that owns the trigger type shuts down, call unregisterTriggerType to remove it from the engine. Any triggers still bound to the type will stop firing.
iii.unregisterTriggerType({ id: 'webhook', description: 'External webhook trigger' })iii_client.unregister_trigger_type({"id": "webhook", "description": "External webhook trigger"})iii.unregister_trigger_type("webhook");Result
Your custom webhook trigger type is registered with the engine. Any function in any worker can bind to it with registerTrigger({ type: 'webhook', ... }), and the engine routes registration and teardown calls to your handler. The same pattern works for any event source — file watchers, message brokers, hardware signals, or anything else you can subscribe to in code.
Built-in trigger types
Before creating a custom type, check the built-in trigger types — http, cron, queue, subscribe, state, and stream cover the most common event sources.