iii-core-primitives

Sets up and runs background functions across different programming languages.

Installation
Run `npx skills add "https://github.com/iii-hq/iii" --skill "iii-core-primitives"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

SKILL.mdShow the author's original SKILL.md
---
name: iii-core-primitives
description: >-
  Use when registering iii functions, binding triggers, observing functions, selecting
  sync/void/enqueue invocation, creating workers, inspecting the live worker registry, installing
  registry workers, authoring event sources, moving channel data, or adapting external HTTP
  functions across TypeScript, Python, and Rust.
---

# Core Primitives

iii has three top-level primitives:

- **Function**: a named unit of work such as `orders::validate`
- **Trigger**: an event source bound to a function
- **Worker**: a process that connects to the engine and executes functions

Since 0.25 every function is also **observable**: `iii.observe(source, target)` invokes `target`
on each invocation of `source` that passes its filters. Observers replace trigger bindings over
time; the built-in providers (`http`, `cron`, queue, `state`, `stream`, pub/sub, database) still
use trigger types in 0.25, so keep `registerTrigger` for them. See `iii-observe`.

Use `::` in function IDs, leading slashes in HTTP `api_path`, and `expression` for cron config.

## Function Registration

Register local handlers when you control the implementation. Register HTTP-invoked functions when
iii should call an existing external endpoint.

| Shape | Use for |
| --- | --- |
| `registerFunction(id, handler, options?)` | Local worker code |
| `registerFunction(id, HttpInvocationConfig, options?)` | Existing HTTP services |
| `registerTrigger({ type, function_id, config, metadata? })` | Binding a built-in event source (deprecated in 0.25, still required for providers that have not migrated) |
| `observe({ function_id, args?, filters? }, target, options?)` | Running `target` after another function's invocations, or on an engine event (`iii-observe`) |
| `trigger({ function_id, payload, action?, timeout? })` | Calling any function by ID |

Functions and triggers can carry metadata for ownership, discovery, and generated skills. Do not put
secrets in metadata.

## Workers and Registry

A worker is any process that connects to the engine and registers functions or trigger types. There
are two common paths:

| Task | Use |
| --- | --- |
| Create your own worker | Write SDK code that calls `registerWorker`, `registerFunction`, and `registerTrigger` or `observe` |
| Add an existing capability | Browse `https://workers.iii.dev/`, then call `compose::add worker=<name>` |
| Pin a worker version | `compose::add worker=<name>@<version>` |
| Declare a local worker | Add `worker: path://./workers/my-worker` under `containers:` |
| Reproduce a project | Commit the exact versions in `worker-compose.yaml` |

The public worker registry at `workers.iii.dev` is for installable workers such as HTTP, state,
queue, pub/sub, cron, database, shell, console, rbac-proxy, and other capability workers.
(`iii-sandbox` and `iii-observability` are engine-owned and are not added through the registry.) Those workers may ship their own function-level skills; do not duplicate every capability
as a top-level iii skill.

### Worker Manifest

Use `iii.worker.yaml` when iii should start a local worker project:

```yaml
name: math-worker
runtime:
  kind: python
  package_manager: pip
  entry: math_worker.py
scripts:
  install: "pip install -r requirements.txt"
  start: "python math_worker.py"
```

The manifest describes how to start the process. Once running, the WebSocket connection and function
registrations are what make the worker part of iii.

### Live Engine Registry

The engine keeps a live registry of connected workers, registered functions, triggers, and trigger
types. Read it through the built-in discovery functions:

| Function | Returns |
| --- | --- |
| `engine::workers::list` | Connected workers and metrics |
| `engine::functions::list` | Registered functions |
| `engine::triggers::list` | Registered triggers |
| `engine::trigger-types::list` | Advertised trigger types and schemas |
| `engine::observers::list` | Observers you may see (`function_id`, `namespace`, `worker` filters) |

For topology changes, observe the engine-hosted sources `engine::on-workers-changed` or
`engine::on-functions-changed` with `deliver: 'input'` (see `iii-observe`). The older
`engine::workers-available` and `engine::functions-available` trigger types still fire.

## Built-In Trigger Shapes

The workers behind these types have not moved to source functions in 0.25, so bind them with
`registerTrigger`. Each call logs a one-time deprecation warning per process; it is informational
and the binding works as before. For `log`, the engine also hosts the `observability::on-log`
source (`iii-observe`).

| Trigger type | Registration config | Handler payload |
| --- | --- | --- |
| `http` | `{ api_path: "/orders/:id", http_method: "POST" }` | `{ query_params, path_params, headers, path, method, body }` |
| `cron` | `{ expression: "0 0 9 * * * *" }` | `{ trigger, job_id, scheduled_time, actual_time }` |
| `durable:subscriber` | `{ topic: "payments" }` | The queued message payload |
| `subscribe` | `{ topic: "orders.created" }` | The published event payload |
| `state` | `{ scope: "orders", key?: "order-123" }` | `{ event_type, scope, key, old_value, new_value }` |
| `stream` | `{ stream_name, group_id, item_id? }` | Stream event details |
| `log` | `{ level: "warn" }` | OpenTelemetry-style log data |

Add `condition_function_id` to built-in trigger config when the handler should only run if a boolean
condition function returns `true`. With `observe`, a condition you can state over the event becomes a
declarative filter; a condition that needs code stays on `registerTrigger` or moves into the target.

## Invocation Modes

| Mode | Shape | Use when |
| --- | --- | --- |
| Sync | `trigger({ function_id, payload })` | The caller needs the result |
| Void | `TriggerAction.Void()` | Optional side effect, no result needed |
| Enqueue | `TriggerAction.Enqueue({ queue })` | Reliable async work with queue policy |

Use enqueue for work that must complete with retries. Use void for analytics, notifications, and
other non-critical side effects.

## Namespaces

Every worker registers in a namespace: the `namespace` init option, else `III_NAMESPACE`, else the
engine's `default`. Compose sets `III_NAMESPACE` to its daemon's namespace for every worker it
starts, so all workers in one `worker-compose.yaml` share a namespace and call each other by bare
function id. Engine-owned functions (`engine::*`, `configuration::*`, `stream::*`) live in `default`;
`engine::*` resolves there implicitly, the others need `namespace: "default"` on the call from a
namespaced worker. Routing is strict and ids are never prefixed with a namespace. From the CLI,
`iii trigger -n <namespace> <function>` selects it. Details and per-language examples are in
`iii-sdk-reference`.

## Code Examples

### TypeScript

```typescript
import { registerWorker, TriggerAction } from "iii-sdk";

const iii = registerWorker("ws://localhost:49134", { workerName: "orders-worker" });

iii.registerFunction("orders::validate", async (order) => {
  if (!order.id) throw new Error("missing order id");
  return { ...order, valid: true };
});

iii.registerFunction("orders::process", async (order) => {
  const validated = await iii.trigger({ function_id: "orders::validate", payload: order });
  await iii.trigger({
    function_id: "orders::charge",
    payload: validated,
    action: TriggerAction.Enqueue({ queue: "payments" }),
  });
  return { accepted: true, orderId: validated.id };
});

iii.registerTrigger({
  type: "http",
  function_id: "orders::process",
  config: { api_path: "/orders", http_method: "POST" },
});
```

### Python

```python
from iii import register_worker

iii = register_worker("ws://localhost:49134")

def validate(order):
    if not order.get("id"):
        raise ValueError("missing order id")
    return {**order, "valid": True}

def process(order):
    validated = iii.trigger({"function_id": "orders::validate", "payload": order})
    iii.trigger({
        "function_id": "orders::charge",
        "payload": validated,
        "action": {"type": "enqueue", "queue": "payments"},
    })
    return {"accepted": True, "orderId": validated["id"]}

iii.register_function("orders::validate", validate)
iii.register_function("orders::process", process)
iii.register_trigger({
    "type": "http",
    "function_id": "orders::process",
    "config": {"api_path": "/orders", "http_method": "POST"},
})
```

### Rust

```rust
use iii_sdk::{register_worker, Error, InitOptions, RegisterFunction, TriggerAction};
use iii_sdk::protocol::{RegisterTriggerInput, TriggerRequest};
use serde_json::{json, Value};

let iii = register_worker("ws://127.0.0.1:49134", InitOptions::default());

iii.register_function("orders::validate", RegisterFunction::new(|order: Value| -> Result<Value, Error> {
    if order["id"].is_null() {
        return Err("missing order id".into());
    }
    Ok(json!({ "valid": true, "order": order }))
}));

let process_client = iii.clone();
iii.register_function("orders::process", RegisterFunction::new_async(move |order: Value| {
    let iii = process_client.clone();
    async move {
        let validated = iii.trigger(TriggerRequest::new("orders::validate", order)).await?;
        iii.trigger(TriggerRequest {
            function_id: "orders::charge".into(),
            payload: validated.clone(),
            action: Some(TriggerAction::Enqueue { queue: "payments".into() }),
            timeout_ms: None,
        }).await?;
        Ok::<_, Error>(json!({ "accepted": true, "order": validated }))
    }
}));

// Deprecated since 0.25 (the http worker still provides a trigger type): compiles with a warning.
#[allow(deprecated)]
iii.register_trigger(RegisterTriggerInput::new(
    "http",
    "orders::process",
    json!({ "api_path": "/orders", "http_method": "POST" }),
))?;
```

## Advanced Primitive Patterns

- **Custom event sources**: since 0.25, register a function with an `observe` descriptor
  (`delivery: 'owner'`, `args_format`, `onObserverAdded` / `onObserverRemoved`) and hand events to
  observers with `obs.emit` or `obs.call` (see `iii-observe`). `registerTriggerType({ id,
  description }, handler)` is deprecated: it still works and logs one warning per process. Keep
  listener setup in `registerTrigger` and cleanup in `unregisterTrigger` if you maintain one.
- **Channels**: use `createChannel()` for binary or streaming data that should not be serialized into
  JSON payloads. Pass `readerRef` or `writerRef` through a function payload.
- **HTTP-invoked functions**: use `HttpInvocationConfig` for legacy APIs, third-party endpoints, or
  immutable services. Use environment variable names for auth fields, not raw secrets.
- **Schemas**: Rust can derive request/response schemas with `schemars::JsonSchema`; Python can use
  type hints or Pydantic; Node can pass JSON Schema manually.

## When to Use

- Use this skill for function registration, trigger binding, trigger payload shapes, invocation mode
  decisions, worker creation, worker registry access, trigger conditions, custom trigger types,
  channels, and HTTP-invoked functions.
- Use it to decide between `registerTrigger` (built-in providers in 0.25) and `observe` (any
  function, engine events, migrated providers).
- Use this when a task spans TypeScript, Python, or Rust examples for the same iii primitive.

## Boundaries

- For `iii.observe`, filters, observable functions, local dispatch, and migrating off
  `registerTrigger` / `registerTriggerType`, use `iii-observe`.
- For engine ports, adapters, engine-owned workers, RBAC (`rbac-proxy`), and deployment config,
  use `iii-engine-config`.
- For SDK-specific package exports and language caveats, use `iii-sdk-reference`.
- For complete backend designs such as workflows, CQRS, agentic systems, and reactive apps, use
  `iii-architecture-patterns`.
- For failed invocations, timeouts, RBAC denials, and retryability, use `iii-error-handling`.
- Worker-backed capability details live with the worker docs, not as top-level iii skills.

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us