본문으로 건너뛰기
cccskills
무료GitHub에 공개

iii-observe

Connect iii function events to other functions, filter which events trigger actions, and react to engine changes. Covers custom event delivery, observer state handling, local execution settings, and migration from deprecated trigger APIs.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

원문원문 설명 보기

Use when observing iii functions with iii.observe, writing filters, reacting to engine events, writing observable functions with owner delivery (onObserverAdded, obs.emit, obs.call), tuning local dispatch, or migrating from the deprecated registerTrigger / registerTriggerType across TypeScript, browser, Python, Rust, and Go.

실행 점수 미검증

설치 방법 보기

이럴 때 유용합니다

  • Trigger alerts for function failures that match specific conditions
  • React to worker connections and configuration changes
  • Build custom event sources for other workers
  • Migrate existing trigger code to observe

포함된 파일(1)

  • SKILL.md19.2 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Observe

Since iii 0.25 every function is observable. iii.observe(source, target, options?) connects a source function to a target function: each event of the source that passes the observer's filters invokes the target. It replaces registerTrigger, and an observe descriptor on registerFunction replaces registerTriggerType. Both old APIs still work in 0.25 (see Migrating from triggers).

PartFieldsNotes
source (left side)function_id, namespace?, args?, filters?, on?, capture?, deliver?args only for owner-delivered functions. on (auto only): success (default), error, complete. capture (auto and obs.emit): default ['input', 'output', 'caller'], [] allowed. deliver: envelope (default), input, output
target (right side)a registered function id (or { function_id, namespace })No inline handler. The observer stays pending until the target is registered
optionsid?, max_fires?, expires_in_ms?, max_in_flight?, metadata?id defaults to a UUID kept across replays. metadata is the sidecar of every activation

The source namespace resolves like trigger(): the worker's namespace, except engine::* and configuration::*, which resolve in default.

Observe a function

Register the target first, then observe. This alerts on failed charges of 500 or more:

import { registerWorker, type Observed } from "iii-sdk";

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

iii.registerFunction("alerts::payment-failed", async (event: Observed<{ amount: number }>) => {
  console.log(event.function_id, event.error?.code, event.input?.amount, event.caller?.worker_name);
  return null; // ignored for auto delivery
});

const failures = iii.observe(
  {
    function_id: "payments::charge",
    on: "error",
    filters: { "input.amount": { $gte: 500 } },
    capture: ["input", "error", "caller"],
  },
  "alerts::payment-failed",
  { max_in_flight: 4 },
);
await failures.ready(); // rejects with ObserveRejection (error.code) on error or disposed
// later: await failures.dispose()
iii.register_function("alerts::payment-failed", lambda event: print(event["error"]["code"]))

failures = iii.observe(
    {"function_id": "payments::charge", "on": "error",
     "filters": {"input.amount": {"$gte": 500}}, "capture": ["input", "error", "caller"]},
    "alerts::payment-failed",
    {"max_in_flight": 4},
)
failures.ready()  # blocks; in async code: await failures.ready_async()
use iii_sdk::{Capture, Error, ObserverOptions, Observed, On, RegisterFunction, Source};
use serde_json::{json, Value};

iii.register_function(
    "alerts::payment-failed",
    RegisterFunction::new(|event: Observed<Value>| {
        println!("{} failed: {:?}", event.function_id, event.error);
        Ok::<_, Error>(())
    }),
);
let failures = iii.observe_with(
    Source::function("payments::charge")
        .on(On::Error)
        .filters(json!({ "input.amount": { "$gte": 500 } }))
        .capture([Capture::Input, Capture::Error, Capture::Caller]),
    "alerts::payment-failed",
    ObserverOptions::default().max_in_flight(4),
);
failures.ready().await?;
client.RegisterFunction("alerts::payment-failed", func(ctx context.Context, data json.RawMessage) (any, error) {
	var ev iii.Observed
	if err := json.Unmarshal(data, &ev); err != nil {
		return nil, err
	}
	log.Printf("%s failed: %s", ev.FunctionID, ev.Error.Code)
	return nil, nil
})
failures, err := client.Observe(iii.Source{
	FunctionID: "payments::charge",
	On:         iii.OnError,
	Filters:    iii.Filters{"input.amount": iii.Op{"$gte": 500}},
	Capture:    []iii.Capture{iii.CaptureInput, iii.CaptureError, iii.CaptureCaller},
}, "alerts::payment-failed", iii.ObserverOptions{MaxInFlight: 4})
if err == nil {
	err = failures.Ready(ctx) // *iii.ObserveRejectionError carries the code
}

The browser SDK (iii-browser-sdk) has the same observe API as Node.

What the target receives

  • Envelope (default deliver): { observer_id, activation_id, function_id, namespace, invocation_id, status, input?, output?, error?, caller?, metadata?, started_at_ms, duration_ms }. Only captured fields are present. deliver: 'input' or 'output' sends that value alone.
  • The observer's options.metadata arrives as the handler's metadata argument (Node and browser second argument, Python metadata parameter, the Option<Value> of a Rust Fn(Value, Option<Value>) handler, Go iii.MetadataFromContext(ctx)).
  • Activations are fire-and-forget and at most once; the target's return value is discarded, except for obs.call (below). Drops are counted, never silent.

Handle and states

pending (no owner, no ack, or no target yet) → active or error. suspended while the target is away, or while the observing worker is disconnected (30 s grace, then disposed). disposed after dispose(), max_fires, or expiry. observe() returns synchronously, works before the connection is up, and is replayed with the same id on reconnect. onStateChange(listener) (Python on_state_change, Go OnStateChange, Rust on_state_change) reports every transition with a reason code.

Filters

Filters are declarative JSON, evaluated by the same iii-filter language in the engine and every SDK. Full reference: https://iii.dev/docs/reference/filters.

  • A clause is "<path>": { "$op": operand }. Explicit operators only: { "input.op": "insert" } is INVALID_FILTER; write { "input.op": { "$eq": "insert" } }. Clauses are ANDed.
  • Roots: input, output, error, caller, metadata. A filter may read fields it does not capture.
  • Operators: $eq $ne $in $nin $gt $gte $lt $lte $exists $prefix $any; combinators $and $or $not. $any matches array elements (operator object, or a filter over @).
  • Only $ne, $nin and $exists: false match an absent path. null is a value. No type coercion; no implicit fan-out over arrays (use $any).
  • Limits: depth 8, 64 nodes, 256 $in items, 16 path segments, 8 KiB as written.
  • Paths are checked against the function's request_format / response_format: an unknown property under a closed schema is INVALID_FILTER_PATH.
{ "caller.parent.worker_name": { "$ne": "search-indexer" },
  "input.items": { "$any": { "@.sku": { "$prefix": "VIP-" } } } }

Observable functions (owner delivery)

A function decides how its observers are delivered with an observe descriptor. Full guide: https://iii.dev/docs/creating-workers/observable-functions.

auto (default)owner
Eventsevery invocationwhat the owner hands over
Argsnone (INVALID_ARGS if sent)validated by the engine against args_format
Deliveryengine or host SDK, after the callobs.emit(event) (fire-and-forget) or obs.call(payload) (returns the target's result)
import { type Observer, ObserveRejection } from "iii-sdk";

const schedules = new Map<string, Set<Observer>>(); // expression -> observers

const onTick = iii.registerFunction(
  "cron::on-tick",
  async (tick: { expression: string; scheduled_time: string }) => {
    for (const obs of schedules.get(tick.expression) ?? []) obs.emit(tick); // false: filters said no
    return null;
  },
  {
    observe: {
      delivery: "owner",
      args_format: { type: "object", required: ["expression"], properties: { expression: { type: "string" } } },
      legacy_trigger_type: "cron", // optional: old registerTrigger({ type: 'cron' }) consumers keep working
      onObserverAdded(obs) {
        const expression = obs.args.expression as string;
        if (!isValidCron(expression)) throw new ObserveRejection("INVALID_CRON_EXPRESSION", expression); // your validator
        if (!schedules.has(expression)) schedules.set(expression, new Set());
        schedules.get(expression)!.add(obs); // index before the ack
      },
      onObserverRemoved(obs) {
        schedules.get(obs.args.expression as string)?.delete(obs);
      },
    },
  },
);
// your scheduler fires: iii.trigger({ function_id: "cron::on-tick", payload: tick }) (runs in-process)
// onTick.isObserved() is one load: skip building events nobody receives
from iii import ObserveHooks, ObserveRejection

schedules: dict[str, set] = {}  # expression -> observers

def on_tick(tick):
    for obs in list(schedules.get(tick["expression"], ())):
        obs.emit(tick)  # False when the observer's filters reject the tick

def added(obs):  # sync or async; raise ObserveRejection(code, message) to refuse
    schedules.setdefault(obs.args["expression"], set()).add(obs)

def removed(obs):
    schedules.get(obs.args["expression"], set()).discard(obs)

iii.register_function("cron::on-tick", on_tick, observe=ObserveHooks(
    delivery="owner",
    args_format={"type": "object", "required": ["expression"],
                 "properties": {"expression": {"type": "string"}}},
    on_observer_added=added,
    on_observer_removed=removed,
))
use iii_sdk::{Error, Observe, ObserveHandler, ObserveRejection, Observer, RegisterFunction};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::{Arc, Mutex}};

#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct CronArgs { expression: String }

#[derive(Serialize, Deserialize, JsonSchema)]
struct CronTick { expression: String, scheduled_time: String }

#[derive(Clone, Default)]
struct Schedules(Arc<Mutex<HashMap<String, Vec<Observer>>>>); // expression -> observers

#[async_trait::async_trait] // ObserveHandler needs the async-trait crate
impl ObserveHandler for Schedules {
    async fn on_observer_added(&self, obs: &Observer) -> Result<(), ObserveRejection> {
        let CronArgs { expression } = obs.args()?; // validated by the engine
        self.0.lock().unwrap().entry(expression).or_default().push(obs.clone());
        Ok(())
    }
    async fn on_observer_removed(&self, obs: &Observer) {
        for list in self.0.lock().unwrap().values_mut() { list.retain(|o| o.id() != obs.id()); }
    }
}

let schedules = Schedules::default();
iii.register_function(
    "cron::on-tick",
    RegisterFunction::new({
        let schedules = schedules.clone();
        move |tick: CronTick| {
            let observers = schedules.0.lock().unwrap().get(&tick.expression).cloned();
            for obs in observers.unwrap_or_default() { obs.emit(&tick)?; } // Ok(false): filters said no
            Ok::<_, Error>(())
        }
    })
    .observe(Observe::owner().args_format::<CronArgs>().handler(schedules)),
);
// onTick is the function's handler; index is your expression -> observers map.
client.RegisterFunction("cron::on-tick", onTick, iii.RegisterFunctionOptions{Observe: &iii.ObserveHooks{
	Delivery:   iii.DeliveryOwner,
	ArgsFormat: json.RawMessage(`{"type":"object","required":["expression"],"properties":{"expression":{"type":"string"}}}`),
	OnObserverAdded: func(ctx context.Context, obs *iii.Observer) error {
		expr, _ := obs.Args["expression"].(string)
		return index.Add(expr, obs) // or iii.ObserveRejection("INVALID_CRON_EXPRESSION", "...")
	},
	OnObserverRemoved: func(ctx context.Context, obs *iii.Observer) { index.Remove(obs) },
}})
// inside onTick: ok, err := obs.Emit(ctx, tick); result, err := obs.Call(ctx, payload)

Rules:

  • onObserverAdded runs once per observer, before the ack; the observer turns active only after it returns. Throwing rejects it (ObserveRejection sets the code, anything else is OBSERVE_REJECTED, 30 s is OWNER_TIMEOUT). onObserverRemoved cannot fail.
  • The owner routes by obs.args and never edits filters: the SDK checks the observer's filters in emit and call. obs.matches(doc) checks them up front.
  • obs.emit returns false when the filters reject the event or the observer is inactive. obs.call rejects with FILTERS_NOT_MATCHED or OBSERVER_NOT_ACTIVE; both throw NOT_OWNER_DELIVERED on an auto function. obs.reject(code, message) is a late rejection.
  • args_format with delivery: 'auto' fails at registration (INVALID_OBSERVE_DESCRIPTOR). allow: false refuses every observer (NOT_OBSERVABLE). obs.legacy is set for observers the engine created from an old registerTrigger (its config replaces the args).

Local dispatch

A worker calling a function it registered itself runs it in-process once the engine confirmed it (functionregistered): no frames when nobody observes it; matching calls are reported in batches. The handler gets a copy of the payload, there is no 16 MiB limit on that path, and a Rust sync handler runs inline on the caller's task. Opt out per call (dispatch: 'remote', Rust .dispatch(Dispatch::Remote), Go Dispatch: iii.DispatchRemote), per function (localDispatch: false, Python local_dispatch=False, Rust .local_dispatch(false), Go LocalDispatch: &off), or engine-wide (observers.local_dispatch: false). Enqueue calls always go through the engine.

Caller and loops

  • caller is { worker_id, worker_name, function_id?, invocation_id?, parent? }. worker_* is stamped by the engine; parent is the caller of the handler the call came from, one level only. Filter on it to stop echo loops: { "caller.parent.worker_name": { "$ne": "indexer" } }.
  • Read the current invocation with iii.currentInvocation() (Python iii.current_invocation(), Rust iii_sdk::current_invocation(), Go iii.InvocationFromContext(ctx)). In Go pass the handler's ctx to Trigger; in Rust code you tokio::spawn loses it unless wrapped in iii_sdk::scope(ctx, fut); in the browser it ends at the first await (use iii.withInvocation(inv, fn)).
  • Every activation is one observe hop deeper. Beyond observers.max_observe_depth (default 8) it is dropped as LOOP_GUARD. A target equal to the observed function is SELF_TARGET.

Engine-hosted event sources

Auto functions in the default namespace whose events the engine emits. They have no output, so observe them with deliver: 'input'. Calling one directly is FORBIDDEN. The old trigger types keep firing.

FunctionReplaces trigger typeinput
engine::on-functions-changedengine::functions-available{ event: "functions_changed", functions: [...] }, bursts coalesced (~100 ms)
engine::on-workers-changedengine::workers-available{ event, worker_id }, event one of worker_connected, worker_disconnected, worker_metadata_updated
configuration::on-updatedconfiguration{ type, event_type, id, name, ..., old_value, new_value }
observability::on-loglogone log record
observability::on-spantrace{ trace_ids: [...] } per window (~300 ms)
iii.observe({ function_id: "engine::on-workers-changed", deliver: "input" }, "ops::on-worker");
iii.observe(
  { function_id: "configuration::on-updated", deliver: "input",
    filters: { "input.id": { $eq: "orders" }, "input.event_type": { $eq: "configuration:updated" } } },
  "orders::reload",
);
// observability::* does not resolve to default automatically: name it from a namespaced worker
iii.observe(
  { function_id: "observability::on-log", namespace: "default", deliver: "input",
    filters: { "input.severity_text": { $eq: "ERROR" } } },
  "alerts::log",
);

Migrating from triggers

registerTrigger and registerTriggerType (every SDK and spelling) are deprecated in 0.25. They keep working, log one warning per process, and will be removed in a future release. Guide: https://iii.dev/docs/upgrading/from-0-24-x.

0.240.25
registerTrigger({ type, function_id, config, metadata })observe({ function_id: <source>, args: config, filters }, function_id, { metadata })
trigger.unregister()handle.dispose()
registerTriggerType({ id }, handler)registerFunction('<worker>::on-<event>', handler, { observe: { delivery: 'owner', ... } })
TriggerHandler.registerTrigger(config) / unregisterTriggeronObserverAdded(obs) (obs.args) / onObserverRemoved(obs)
provider fan-out with iii.trigger(binding.function_id)obs.emit(event) or obs.call(payload)
trigger_request_format / call_request_formatobserve.args_format / the function's request_format
condition_function_idfilters for declarative conditions; a condition that needs code stays on registerTrigger or moves into the target

The built-in providers have not migrated in 0.25. The http, cron, queue (durable:subscriber), state, stream, pubsub (subscribe) and database workers still register trigger types and do not host source functions (http::on-request, cron::on-tick, ...) yet. Keep registerTrigger for them; the warning is informational. Use observe for regular functions, the engine-hosted sources above, and providers that ship source functions. A provider that migrates declares legacy_trigger_type so existing registerTrigger consumers keep working.

Error codes

WhenCodes
Creating an observer (ready() rejects)UNSUPPORTED (engine older than 0.25, or an unknown on/capture/deliver), INVALID_ARGS, INVALID_FILTER, INVALID_FILTER_PATH, NOT_OBSERVABLE, FORBIDDEN, SELF_TARGET, LIMIT_EXCEEDED, OBSERVER_ID_CONFLICT, OWNER_TIMEOUT, OBSERVE_REJECTED, or the owner's own code
obs.callFILTERS_NOT_MATCHED, OBSERVER_NOT_ACTIVE, NOT_OWNER_DELIVERED, OBSERVER_NOT_FOUND, NOT_FUNCTION_OWNER, TARGET_MISMATCH, LOOP_GUARD, OVERFLOW, TIMEOUT, or the target's error
State reasonsOWNER_UNAVAILABLE, TARGET_UNAVAILABLE, OBSERVER_DISCONNECTED, EXPIRED, MAX_FIRES, UNOBSERVED
Drop reasons (metrics)INACTIVE, LOOP_GUARD, MAX_FIRES, OVERFLOW, REPORTER_NOT_OWNER, CANCELLED, INVALID_REPORT, BUFFER_FULL, TOO_LARGE

Inspect observers with engine::observers::list({ function_id?, namespace?, worker? }) and engine::observers::get({ id }); engine::observers::dispose({ id }) removes one. Docs: https://iii.dev/docs/using-iii/observers.

When to Use

  • Use this skill to run a function when another function is called, fails, or completes, to react to engine events (functions, workers, configuration, logs, spans), to write or debug filters, or to build an event source other workers observe.
  • Use it when migrating registerTrigger / registerTriggerType code to 0.25, or when a deprecation warning about them appears.

Boundaries

  • To bind functions to http, cron, queue, state, stream, pub/sub, or database events in 0.25, use registerTrigger as taught in iii-core-primitives; those providers have not migrated.
  • For the observers: engine config section and RBAC hooks, use iii-engine-config.
  • For package exports and language syntax, use iii-sdk-reference; for error handling in general, use iii-error-handling.
  • Do not invent source functions (http::on-request, cron::on-tick, database::on-row-changed) as available: they exist only once the provider worker ships them.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Manage shared iii configuration with JSON Schema, a definition of allowed value structures. Covers validated settings, automatic change notifications, YAML edits, temporary overrides, and default initialization that preserves existing values.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Sharing validated settings between workers
  • Reacting to manual YAML configuration edits
  • Initializing defaults while preserving existing values
iii-hq/iii1.9만2026년 10월 25일 업데이트

iii

무료

Helps extend an iii system by checking live functions, searching the public worker registry, and writing a worker when needed. Covers TypeScript SDK calls, event triggers, error handling, and worker operation through Compose.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Finding and calling functions already available in iii
  • Searching for and adding a published worker
  • Building a custom worker with the TypeScript SDK
iii-hq/iii1.9만2026년 10월 25일 업데이트

Design iii backends by combining functions, queues, and shared state. Choose patterns for workflows with retries, live updates, cooperating AI agents, and event-driven processing, with examples showing how the pieces connect.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Designing order workflows with retries and step tracking
  • Updating screens or caches when stored data changes
  • Passing tasks between specialized AI agents
iii-hq/iii1.9만2026년 10월 25일 업데이트

Guides development of iii functions, event triggers, and workers, the processes that execute functions. Helps choose result-returning calls, one-way calls, or queued work, inspect live registrations, and connect existing HTTP services.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Running a function on an HTTP request or schedule
  • Choosing between direct calls and queued work
  • Inspecting registered workers and functions
iii-hq/iii1.9만2026년 10월 25일 업데이트

Helps configure an iii engine for Compose management or direct supervision by tools such as systemd. Covers configuration file structure, ports, storage, and role-based access control through rbac-proxy, including requirements for public connections.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Configuring an engine managed by Compose
  • Configuring an engine supervised by systemd or Kubernetes
  • Setting authentication and access rules for public connections
iii-hq/iii1.9만2026년 10월 25일 업데이트

Diagnose iii invocation failures using error codes across Node, Python, Rust, and browser workers. Distinguish permission denials, timeouts, and handler failures, then choose appropriate exception handling and decide whether retrying is suitable.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Diagnosing failed function calls by error code
  • Deciding whether a timed-out operation should be retried
  • Handling permission denials without blind retries
iii-hq/iii1.9만2026년 10월 25일 업데이트

iii-hq의 스킬 모두 보기

이 스킬의 문제 신고