Use when composing iii primitives into backend architectures: durable workflows, reactive backends, observer side effects, agentic pipelines, event-driven CQRS, effect pipelines, and trigger-transform-action automation.
76%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/iii-architecture-patterns/SKILL.mdUse this skill when the task is larger than one function or trigger. Pick the pattern from the
requirement, then implement it with Function, Trigger, Worker, observers, state, queues,
streams, and pub/sub.
| Requirement | Pattern | iii shape |
|---|---|---|
| Sequential work with retries, DLQ, step tracking | Durable workflow | Functions chained through named queues, progress in state |
| Keep views, metrics, cache, or clients in sync | Reactive backend | State triggers plus stream/pubsub side effects |
| React to another function's calls, results, or failures without changing it | Observer side effect | iii.observe({ function_id, on, filters }, target) (iii-observe) |
| Specialized AI agents hand work to each other | Agentic backend | One function per agent, queue handoffs, shared state |
| Commands publish events and projections update independently | Event-driven CQRS | Command functions, event log in state, subscribe triggers |
| Pure, traceable composition | Effect pipeline | Small functions composed synchronously with trigger() |
| Webhook/cron automation chains | Low-code automation | Trigger, transform, action nodes chained by enqueue |
Use named queues when steps need different retry, FIFO, or concurrency policy.
import { registerWorker, TriggerAction } from "iii-sdk";
const iii = registerWorker("ws://localhost:49134", { workerName: "order-workflow" });
async function track(orderId: string, step: string, status: string) {
await iii.trigger({
function_id: "state::update",
payload: { scope: "orders", key: orderId, ops: [{ type: "merge", path: ["steps"], value: { [step]: status } }] },
});
}
iii.registerFunction("orders::validate", async (order) => {
await track(order.id, "validate", "done");
return iii.trigger({
function_id: "orders::charge",
payload: order,
action: TriggerAction.Enqueue({ queue: "order-payment" }),
});
});
iii.registerFunction("orders::charge", async (order) => {
await track(order.id, "payment", "done");
return iii.trigger({
function_id: "orders::ship",
payload: order,
action: TriggerAction.Enqueue({ queue: "order-ship" }),
});
});from iii import register_worker
iii = register_worker("ws://localhost:49134")
def track(order_id, step, status):
iii.trigger({
"function_id": "state::update",
"payload": {
"scope": "orders",
"key": order_id,
"ops": [{"type": "merge", "path": ["steps"], "value": {step: status}}],
},
})
def validate(order):
track(order["id"], "validate", "done")
return iii.trigger({
"function_id": "orders::charge",
"payload": order,
"action": {"type": "enqueue", "queue": "order-payment"},
})
iii.register_function("orders::validate", validate)use iii_sdk::TriggerAction;
use iii_sdk::protocol::TriggerRequest;
use serde_json::json;
async fn enqueue_charge(iii: iii_sdk::IIIClient, order: serde_json::Value) -> Result<serde_json::Value, iii_sdk::Error> {
iii.trigger(TriggerRequest {
function_id: "state::update".into(),
payload: json!({
"scope": "orders",
"key": order["id"],
"ops": [{ "type": "merge", "path": ["steps"], "value": { "validate": "done" } }]
}),
action: None,
timeout_ms: None,
}).await?;
iii.trigger(TriggerRequest {
function_id: "orders::charge".into(),
payload: order,
action: Some(TriggerAction::Enqueue { queue: "order-payment".into() }),
timeout_ms: None,
}).await
}Use state triggers when the requirement says "after create/update, do this", "avoid polling", or "push live updates".
iii.registerFunction("todos::on-change", async (event) => {
await iii.trigger({
function_id: "stream::send",
payload: { stream_name: "todos-live", group_id: "default", data: event.new_value },
action: TriggerAction.Void(),
});
});
iii.registerTrigger({
type: "state",
function_id: "todos::on-change",
config: { scope: "todos" },
});Use an observer when "after X runs (or fails), also do Y" and X should not know about Y: audit logs, alerts, projections, cache invalidation. Every function is observable since 0.25, and an observer failure never affects the observed call.
import type { Observed } from "iii-sdk";
iii.registerFunction("audit::order-created", async (event: Observed) => {
await iii.trigger({
function_id: "state::set",
payload: { scope: "audit", key: event.invocation_id, value: { order: event.input, by: event.caller?.worker_name } },
});
return null;
});
// every successful orders::create, except the replay tool's own calls
iii.observe(
{ function_id: "orders::create", filters: { "caller.worker_name": { $ne: "order-replayer" } } },
"audit::order-created",
{ max_in_flight: 8 },
);def notify(event): # the Observed envelope: input, error, caller, ...
print("charge failed", event["input"], event["error"]["code"])
iii.register_function("alerts::payment-failed", notify)
iii.observe(
{"function_id": "payments::charge", "on": "error", "filters": {"input.amount": {"$gte": 500}}},
"alerts::payment-failed",
)Activations are at most once and fire-and-forget. When the side effect must complete with retries,
have the target enqueue the work (TriggerAction.Enqueue). Filter on caller or
caller.parent to stop echo loops; the engine also drops activations deeper than
observers.max_observe_depth (8 by default).
Model each agent as a function with one responsibility. Store shared context in state and hand off work through named queues.
iii.registerFunction("agents::researcher", async (task) => {
await iii.trigger({
function_id: "state::set",
payload: { scope: "research", key: task.id, value: { findings: [] } },
});
return iii.trigger({
function_id: "agents::critic",
payload: task,
action: TriggerAction.Enqueue({ queue: "agent-tasks" }),
});
});Commands validate and publish domain events. Projections subscribe independently and write query-optimized state.
iii.registerFunction("cmd::add-inventory-item", async (input) => {
const event = { type: "inventory.item-added", itemId: input.itemId, quantity: input.quantity };
await iii.trigger({
function_id: "state::set",
payload: { scope: "inventory-events", key: `${Date.now()}-${input.itemId}`, value: event },
});
await iii.trigger({ function_id: "publish", payload: { topic: event.type, data: event } });
return { accepted: true };
});
iii.registerTrigger({
type: "subscribe",
function_id: "proj::inventory-list",
config: { topic: "inventory.item-added" },
});
// Alternative without pub/sub: the projection observes the command function directly and
// receives its input (the command) on every successful call.
iii.observe({ function_id: "cmd::add-inventory-item", deliver: "input" }, "proj::inventory-list");registerTrigger is deprecated in 0.25 (one warning per process) but stays the way to bind the
built-in subscribe, state, cron, http, and queue providers, which have not migrated to
source functions yet.
iii-core-primitives.iii-observe.rbac-proxy), use iii-engine-config;
queue retry and FIFO policy live with the queue worker's docs.iii-sdk-reference.iii-hq/iii
618a6fb
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.