Skip to main content

Overview

Every call to trigger() can behave in one of three fundamentally different ways depending on the action you pass. Choosing the right action determines whether the caller blocks, whether the work is queued with retries, or whether the message is simply dispatched and forgotten.
ActionCaller blocks?Retries?Returns
(none) — SynchronousYesNoFunction result
Void — Fire-and-forgetNoNoNone / null
Enqueue — Named queueNoYes{ messageReceiptId }
Understanding these three modes is critical because they affect latency, reliability, ordering, and error handling across your entire system.

The Three Trigger Actions

1. Synchronous (no action)

When you omit the action field, trigger() performs a direct, synchronous invocation. The caller sends the request to the engine, the engine routes it to the target function, and the caller blocks until the function returns a result or the timeout expires.
import { registerWorker } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

const result = await iii.trigger({
  function_id: 'users::get',
  payload: { id: 'usr_42' },
  timeoutMs: 5000,
})

console.log(result.name) // "Alice"
When to use synchronous triggers:
  • You need the function’s return value to continue (e.g. fetching data, validating input)
  • The operation is fast and the caller can afford to wait
  • You want errors to propagate directly to the caller
  • Request/response APIs like HTTP endpoints that must return data to a client

2. Void (fire-and-forget)

TriggerAction.Void() tells the engine to dispatch the invocation but not wait for a result. The caller continues immediately. If the target function fails, the caller is unaware — there are no retries and no acknowledgement.
import { registerWorker, TriggerAction } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

await iii.trigger({
  function_id: 'analytics::track-event',
  payload: { event: 'page_view', userId: 'usr_42', page: '/dashboard' },
  action: TriggerAction.Void(),
})
// Execution continues immediately — no waiting for analytics to complete
When to use Void:
  • The caller does not need a response
  • Losing the occasional message is acceptable (best-effort delivery)
  • You want minimal latency impact on the caller’s hot path
  • Side effects like logging, analytics, non-critical notifications

3. Enqueue (named queue)

TriggerAction.Enqueue({ queue: 'name' }) routes the invocation through a named queue configured in iii-config.yaml, check how to create queues in more detail. The caller receives an acknowledgement (messageReceiptId) once the engine accepts the job but does not wait for it to be processed. The queue provides retries, concurrency control, backoff, and optional FIFO ordering. If all retries are exhausted, the job moves to a dead letter queue.
import { registerWorker, TriggerAction } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

const receipt = await iii.trigger({
  function_id: 'orders::process-payment',
  payload: { orderId: 'ord_789', amount: 149.99, currency: 'USD' },
  action: TriggerAction.Enqueue({ queue: 'payment' }),
})

console.log(receipt.messageReceiptId) // "msg_abc123"
When to use Enqueue:
  • The work is expensive or slow and you do not want to block the caller
  • You need automatic retries with backoff on failure
  • You need concurrency control over how many jobs run in parallel
  • You need FIFO ordering guarantees (e.g. financial transactions)
  • You want failed jobs preserved in a dead letter queue for later inspection

Key Differences at a Glance

DimensionSynchronousVoidEnqueue
Caller blocksYes — waits for resultNoNo
ReturnsFunction return valuenull / None{ messageReceiptId }
Error propagationErrors reach the caller directlyErrors are silent to the callerRetried automatically; DLQ on exhaustion
RetriesNone — caller handles retry logicNoneConfigurable (max_retries, backoff_ms)
OrderingSequential by natureNo guaranteesOptional FIFO with message_group_field
Concurrency controlN/AN/AConfigurable per queue
Use caseRead data, validate, RPCAnalytics, logs, non-critical side effectsPayments, emails, heavy processing

Real-World Scenarios

Scenario 1: E-Commerce Order Flow

An order API must respond fast, payment processing must be reliable, and analytics can be best-effort.
import { registerWorker, TriggerAction, Logger } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

iii.registerFunction({ id: 'orders::create' }, async (req) => {
  const logger = new Logger()
  const order = { id: crypto.randomUUID(), ...req.body }

  // Reliable payment processing — enqueued with retries
  await iii.trigger({
    function_id: 'orders::process-payment',
    payload: order,
    action: TriggerAction.Enqueue({ queue: 'payment' }),
  })

  // Best-effort analytics — fire and forget
  await iii.trigger({
    function_id: 'analytics::track',
    payload: { event: 'order_created', orderId: order.id },
    action: TriggerAction.Void(),
  })

  logger.info('Order created', { orderId: order.id })
  return { status_code: 201, body: { orderId: order.id } }
})

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

Scenario 2: User Registration Pipeline

Registration must return the created user (synchronous), send a welcome email reliably (enqueue), and log the event without blocking (void).
import { registerWorker, TriggerAction } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

iii.registerFunction({ id: 'users::register' }, async (req) => {
  const { email, name } = req.body

  // Synchronous — need the validation result before proceeding
  const validation = await iii.trigger({
    function_id: 'users::validate-email',
    payload: { email },
  })

  if (!validation.valid) {
    return { status_code: 400, body: { error: 'Invalid email' } }
  }

  const user = await createUserInDb({ email, name })

  // Enqueue — welcome email must be delivered reliably
  await iii.trigger({
    function_id: 'emails::send-welcome',
    payload: { userId: user.id, email, name },
    action: TriggerAction.Enqueue({ queue: 'email' }),
  })

  // Void — audit log is best-effort
  await iii.trigger({
    function_id: 'audit::log',
    payload: { action: 'user_registered', userId: user.id },
    action: TriggerAction.Void(),
  })

  return { status_code: 201, body: { userId: user.id } }
})

Scenario 3: Multi-Step Data Pipeline

An ETL pipeline where each stage hands off to the next via queues, with monitoring dispatched as void.
import { registerWorker, TriggerAction, Logger } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

iii.registerFunction({ id: 'etl::extract' }, async () => {
  const logger = new Logger()
  const rawData = await fetchFromSource()

  // Reliable handoff — transform stage must not lose data
  await iii.trigger({
    function_id: 'etl::transform',
    payload: { records: rawData, extractedAt: Date.now() },
    action: TriggerAction.Enqueue({ queue: 'etl-transform' }),
  })

  // Best-effort monitoring
  await iii.trigger({
    function_id: 'monitoring::report',
    payload: { stage: 'extract', recordCount: rawData.length },
    action: TriggerAction.Void(),
  })

  logger.info('Extraction complete', { records: rawData.length })
})

iii.registerTrigger({
  type: 'cron',
  function_id: 'etl::extract',
  config: { expression: '0 * * * *' },
})

Decision Flowchart

Use this mental model when deciding which action to use:

Combining Actions in a Single Function

A single function can use all three actions. This is common in orchestrator functions that coordinate multiple downstream services.
import { registerWorker, TriggerAction, Logger } from 'iii-sdk'

const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134')

iii.registerFunction({ id: 'checkout::process' }, async (cart) => {
  const logger = new Logger()

  // 1. Synchronous — validate inventory before charging
  const inventory = await iii.trigger({
    function_id: 'inventory::check',
    payload: { items: cart.items },
  })

  if (!inventory.available) {
    return { status_code: 409, body: { error: 'Items out of stock' } }
  }

  // 2. Synchronous — charge the customer (need confirmation)
  const charge = await iii.trigger({
    function_id: 'payments::charge',
    payload: { amount: cart.total, paymentMethod: cart.paymentMethod },
  })

  // 3. Enqueue — fulfillment is slow but must complete
  await iii.trigger({
    function_id: 'fulfillment::ship',
    payload: { orderId: charge.orderId, items: cart.items },
    action: TriggerAction.Enqueue({ queue: 'fulfillment' }),
  })

  // 4. Enqueue — confirmation email must be delivered
  await iii.trigger({
    function_id: 'emails::order-confirmation',
    payload: { email: cart.email, orderId: charge.orderId },
    action: TriggerAction.Enqueue({ queue: 'email' }),
  })

  // 5. Void — analytics can be best-effort
  await iii.trigger({
    function_id: 'analytics::track',
    payload: { event: 'checkout_complete', orderId: charge.orderId },
    action: TriggerAction.Void(),
  })

  logger.info('Checkout complete', { orderId: charge.orderId })
  return { status_code: 200, body: { orderId: charge.orderId } }
})

SDK Syntax Reference

import { TriggerAction } from 'iii-sdk'

// Synchronous (default)
const result = await iii.trigger({
  function_id: 'my-function',
  payload: { key: 'value' },
})

// Void
await iii.trigger({
  function_id: 'my-function',
  payload: { key: 'value' },
  action: TriggerAction.Void(),
})

// Enqueue
const receipt = await iii.trigger({
  function_id: 'my-function',
  payload: { key: 'value' },
  action: TriggerAction.Enqueue({ queue: 'my-queue' }),
})

Common Mistakes

If you call a slow function synchronously inside an HTTP handler, your API response time degrades. Use Enqueue for work that does not need to complete before responding.
// Bad — blocks the HTTP response for 10+ seconds
const report = await iii.trigger({
  function_id: 'reports::generate-pdf',
  payload: { userId: 'usr_42' },
})

// Good — respond immediately, process later
await iii.trigger({
  function_id: 'reports::generate-pdf',
  payload: { userId: 'usr_42' },
  action: TriggerAction.Enqueue({ queue: 'reports' }),
})
Void provides no delivery guarantees. If the target function fails or the worker is unavailable, the message is lost. Use Enqueue when reliability matters.
// Bad — if the email service is down, the receipt is lost forever
await iii.trigger({
  function_id: 'emails::send-receipt',
  payload: receiptData,
  action: TriggerAction.Void(),
})

// Good — queue ensures retry on failure
await iii.trigger({
  function_id: 'emails::send-receipt',
  payload: receiptData,
  action: TriggerAction.Enqueue({ queue: 'email' }),
})
Enqueue returns a receipt, not the function’s result. If you need the function’s return value, use a synchronous call.
// Bad — receipt does not contain the user data
const receipt = await iii.trigger({
  function_id: 'users::get',
  payload: { id: 'usr_42' },
  action: TriggerAction.Enqueue({ queue: 'default' }),
})
// receipt.messageReceiptId — not what you wanted

// Good — synchronous call returns the user
const user = await iii.trigger({
  function_id: 'users::get',
  payload: { id: 'usr_42' },
})

Next Steps

Use Queues

Configure named queues with retries, concurrency, and FIFO ordering

Dead Letter Queues

Handle and redrive failed queue messages

Functions & Triggers

Register functions and bind triggers to them

Trigger Types

Deep dive into HTTP, queue, cron, log, and stream triggers