From 6e91133d48509acb3484c0245d6d05efa0e6b69b Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 10:33:50 -0300 Subject: [PATCH 01/11] fix(engine): propagate trigger registration errors to worker (path A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace anyhow::Error in TriggerRegistry::register_trigger with a structured RegisterTriggerError enum (UnknownBuiltin / Unknown / Other). Engine no longer swallows the registry error at the RegisterTrigger router arm — on failure it sends a TriggerRegistrationResult with an ErrorBody back to the worker that initiated the request. Built-in trigger types (http, cron, ...) include the "iii worker add " install hint in the message. --- engine/src/engine/mod.rs | 115 +++++++++++++++++++++++++++++++++++++-- engine/src/trigger.rs | 46 +++++++++++----- 2 files changed, 141 insertions(+), 20 deletions(-) diff --git a/engine/src/engine/mod.rs b/engine/src/engine/mod.rs index 9515fe62b..d9f83b474 100644 --- a/engine/src/engine/mod.rs +++ b/engine/src/engine/mod.rs @@ -770,17 +770,44 @@ impl Engine { reg_function_id = format!("{prefix}::{reg_function_id}"); } - let _ = self + match self .trigger_registry .register_trigger(Trigger { - id: reg_trigger_id, - trigger_type: reg_trigger_type, - function_id: reg_function_id, + id: reg_trigger_id.clone(), + trigger_type: reg_trigger_type.clone(), + function_id: reg_function_id.clone(), config: reg_config, worker_id: Some(worker.id), metadata: metadata.clone(), }) - .await; + .await + { + Ok(()) => {} + Err(err) => { + let error_body = match &err { + crate::trigger::RegisterTriggerError::UnknownBuiltin { .. } + | crate::trigger::RegisterTriggerError::Unknown { .. } => { + crate::protocol::ErrorBody::new( + "trigger_type_not_found", + err.to_string(), + ) + } + crate::trigger::RegisterTriggerError::Other(_) => { + crate::protocol::ErrorBody::new( + "trigger_registration_failed", + err.to_string(), + ) + } + }; + let result_msg = Message::TriggerRegistrationResult { + id: reg_trigger_id, + trigger_type: reg_trigger_type, + function_id: reg_function_id, + error: Some(error_body), + }; + let _ = self.send_msg(worker, result_msg).await; + } + } crate::workers::telemetry::collector::track_trigger_registered(); Ok(()) @@ -3271,6 +3298,84 @@ mod tests { .expect("TriggerRegistrationResult with error should succeed"); } + #[tokio::test] + async fn test_register_trigger_unknown_builtin_sends_install_hint() { + ensure_default_meter(); + let engine = Engine::new(); + let (tx, mut rx) = mpsc::channel::(8); + let worker = WorkerConnection::new(tx); + + let msg = Message::RegisterTrigger { + id: "trig-1".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-1".to_string(), + config: serde_json::json!({}), + metadata: None, + }; + + engine + .router_msg(&worker, &msg) + .await + .expect("RegisterTrigger should succeed at protocol level"); + + let outbound = rx + .try_recv() + .expect("engine should emit TriggerRegistrationResult on failure"); + let Outbound::Protocol(Message::TriggerRegistrationResult { + id, + trigger_type, + function_id, + error, + }) = outbound + else { + panic!("expected TriggerRegistrationResult, got {:?}", outbound); + }; + assert_eq!(id, "trig-1"); + assert_eq!(trigger_type, "http"); + assert_eq!(function_id, "fn-1"); + let err = error.expect("error should be populated"); + assert_eq!(err.code, "trigger_type_not_found"); + assert!(err.message.contains("iii-http"), "msg: {}", err.message); + assert!( + err.message.contains("iii worker add"), + "msg: {}", + err.message + ); + } + + #[tokio::test] + async fn test_register_trigger_unknown_type_sends_generic_error() { + ensure_default_meter(); + let engine = Engine::new(); + let (tx, mut rx) = mpsc::channel::(8); + let worker = WorkerConnection::new(tx); + + let msg = Message::RegisterTrigger { + id: "trig-2".to_string(), + trigger_type: "totally-made-up".to_string(), + function_id: "fn-2".to_string(), + config: serde_json::json!({}), + metadata: None, + }; + + engine + .router_msg(&worker, &msg) + .await + .expect("RegisterTrigger should succeed at protocol level"); + + let outbound = rx.try_recv().expect("engine should emit a result"); + let Outbound::Protocol(Message::TriggerRegistrationResult { error, .. }) = outbound else { + panic!("expected TriggerRegistrationResult"); + }; + let err = error.expect("error should be populated"); + assert_eq!(err.code, "trigger_type_not_found"); + assert!( + err.message.contains("totally-made-up"), + "msg should name the missing type: {}", + err.message + ); + } + // ========================================================================= // router_msg: RegisterService // ========================================================================= diff --git a/engine/src/trigger.rs b/engine/src/trigger.rs index ac5c5c882..c4679151e 100644 --- a/engine/src/trigger.rs +++ b/engine/src/trigger.rs @@ -32,6 +32,21 @@ fn worker_name_for_trigger_type(trigger_type_id: &str) -> Option<&'static str> { .map(|(_, worker)| *worker) } +#[derive(Debug, thiserror::Error)] +pub enum RegisterTriggerError { + #[error( + "Trigger type \"{trigger_type}\" not found — worker {worker} is missing. Run: iii worker add {worker}" + )] + UnknownBuiltin { + trigger_type: String, + worker: &'static str, + }, + #[error("Trigger type \"{trigger_type}\" not found")] + Unknown { trigger_type: String }, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + pub struct TriggerType { pub id: String, pub _description: String, @@ -231,7 +246,7 @@ impl TriggerRegistry { Ok(()) } - pub async fn register_trigger(&self, trigger: Trigger) -> Result<(), anyhow::Error> { + pub async fn register_trigger(&self, trigger: Trigger) -> Result<(), RegisterTriggerError> { let trigger_type_id = trigger.trigger_type.clone(); let Some(trigger_type) = self.trigger_types.get(&trigger_type_id) else { if let Some(worker_name) = worker_name_for_trigger_type(&trigger_type_id) { @@ -241,28 +256,25 @@ impl TriggerRegistry { worker_name.cyan().bold(), format!("iii worker add {}", worker_name).green().bold() ); - return Err(anyhow::anyhow!( - "Trigger type \"{}\" not found — worker {} is missing. Run: iii worker add {}", - trigger_type_id, - worker_name, - worker_name - )); + return Err(RegisterTriggerError::UnknownBuiltin { + trigger_type: trigger_type_id, + worker: worker_name, + }); } tracing::error!("Trigger type {} not found", trigger_type_id.purple()); - return Err(anyhow::anyhow!("Trigger type not found")); + return Err(RegisterTriggerError::Unknown { + trigger_type: trigger_type_id, + }); }; - match trigger_type + if let Err(err) = trigger_type .registrator .register_trigger(trigger.clone()) .await { - Ok(_) => {} - Err(err) => { - tracing::error!(error = %err, "Error registering trigger"); - return Err(err); - } + tracing::error!(error = %err, "Error registering trigger"); + return Err(RegisterTriggerError::Other(err)); } drop(trigger_type); @@ -475,7 +487,11 @@ mod tests { let trigger = make_trigger("t1", "nonexistent"); let result = registry.register_trigger(trigger).await; assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "Trigger type not found"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("\"nonexistent\" not found"), + "Expected unknown-type message, got: {err_msg}" + ); assert!(registry.triggers.is_empty()); } From 61d109427bdeb7c049b69e22d7721657740c386b Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 10:35:51 -0300 Subject: [PATCH 02/11] fix(engine): forward registrator trigger errors to originator (path B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TriggerRegistrationResult router arm was a no-op, dropping errors that registrator workers (iii-http, iii-cron, ...) reported when they rejected a trigger. Engine now looks up the originating worker via Trigger.worker_id and forwards the result. Failed triggers are also removed from the registry so they don't accumulate. Successful results are not forwarded — they would flood the user worker with chatter and are not actionable. --- engine/src/engine/mod.rs | 174 +++++++++++++++++++++++++++++++++------ 1 file changed, 149 insertions(+), 25 deletions(-) diff --git a/engine/src/engine/mod.rs b/engine/src/engine/mod.rs index d9f83b474..85cd6cdd0 100644 --- a/engine/src/engine/mod.rs +++ b/engine/src/engine/mod.rs @@ -606,6 +606,49 @@ impl Engine { error, } => { tracing::debug!(id = %id, trigger_type = %trigger_type, function_id = %function_id, error = ?error, "TriggerRegistrationResult"); + + let Some(trigger_entry) = self.trigger_registry.triggers.get(id) else { + tracing::debug!( + trigger_id = %id, + "TriggerRegistrationResult for unknown trigger; ignoring" + ); + return Ok(()); + }; + let originator_id = trigger_entry.worker_id; + drop(trigger_entry); + + if error.is_some() { + self.trigger_registry.triggers.remove(id); + } + + if error.is_none() { + return Ok(()); + } + + let Some(originator_id) = originator_id else { + tracing::debug!( + trigger_id = %id, + "TriggerRegistrationResult for trigger without originator; ignoring" + ); + return Ok(()); + }; + + let Some(originator) = self.worker_registry.get_worker(&originator_id) else { + tracing::debug!( + trigger_id = %id, + originator = %originator_id, + "TriggerRegistrationResult originator no longer connected; dropping" + ); + return Ok(()); + }; + + let forward = Message::TriggerRegistrationResult { + id: id.clone(), + trigger_type: trigger_type.clone(), + function_id: function_id.clone(), + error: error.clone(), + }; + let _ = self.send_msg(&originator, forward).await; Ok(()) } Message::RegisterTriggerType { @@ -3248,54 +3291,135 @@ mod tests { } #[tokio::test] - async fn test_router_msg_trigger_registration_result_is_noop() { + async fn test_trigger_registration_result_forwards_error_to_originator() { ensure_default_meter(); let engine = Engine::new(); - let (tx, mut rx) = mpsc::channel::(8); - let worker = WorkerConnection::new(tx); + + let (user_tx, mut user_rx) = mpsc::channel::(8); + let user = WorkerConnection::new(user_tx); + engine.worker_registry.register_worker(user.clone()); + + let (registrator_tx, _registrator_rx) = mpsc::channel::(8); + let registrator = WorkerConnection::new(registrator_tx); + + engine.trigger_registry.triggers.insert( + "trig-1".to_string(), + crate::trigger::Trigger { + id: "trig-1".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-1".to_string(), + config: serde_json::json!({}), + worker_id: Some(user.id), + metadata: None, + }, + ); let msg = Message::TriggerRegistrationResult { - id: "trigger-1".to_string(), - trigger_type: "my-type".to_string(), - function_id: "my-func".to_string(), + id: "trig-1".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-1".to_string(), + error: Some(crate::protocol::ErrorBody::new( + "invalid_config", + "api_path is required", + )), + }; + + engine + .router_msg(®istrator, &msg) + .await + .expect("router_msg should succeed"); + + let outbound = user_rx + .try_recv() + .expect("originator should receive forwarded TriggerRegistrationResult"); + let Outbound::Protocol(Message::TriggerRegistrationResult { + id, + trigger_type, + function_id, + error, + }) = outbound + else { + panic!("expected TriggerRegistrationResult, got {:?}", outbound); + }; + assert_eq!(id, "trig-1"); + assert_eq!(trigger_type, "http"); + assert_eq!(function_id, "fn-1"); + let err = error.expect("error should be populated"); + assert_eq!(err.code, "invalid_config"); + assert_eq!(err.message, "api_path is required"); + + assert!( + engine.trigger_registry.triggers.get("trig-1").is_none(), + "failed trigger should be removed from registry" + ); + } + + #[tokio::test] + async fn test_trigger_registration_result_success_does_not_forward_or_remove() { + ensure_default_meter(); + let engine = Engine::new(); + + let (user_tx, mut user_rx) = mpsc::channel::(8); + let user = WorkerConnection::new(user_tx); + engine.worker_registry.register_worker(user.clone()); + + let (registrator_tx, _registrator_rx) = mpsc::channel::(8); + let registrator = WorkerConnection::new(registrator_tx); + + engine.trigger_registry.triggers.insert( + "trig-2".to_string(), + crate::trigger::Trigger { + id: "trig-2".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-2".to_string(), + config: serde_json::json!({}), + worker_id: Some(user.id), + metadata: None, + }, + ); + + let msg = Message::TriggerRegistrationResult { + id: "trig-2".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-2".to_string(), error: None, }; engine - .router_msg(&worker, &msg) + .router_msg(®istrator, &msg) .await - .expect("TriggerRegistrationResult should succeed"); + .expect("router_msg should succeed"); - // Should not produce any response assert!( - rx.try_recv().is_err(), - "TriggerRegistrationResult should not produce any outbound message" + user_rx.try_recv().is_err(), + "success result should not be forwarded" + ); + + assert!( + engine.trigger_registry.triggers.get("trig-2").is_some(), + "successful trigger should remain in registry" ); } #[tokio::test] - async fn test_router_msg_trigger_registration_result_with_error() { + async fn test_trigger_registration_result_unknown_trigger_id_is_noop() { ensure_default_meter(); let engine = Engine::new(); - let (tx, _rx) = mpsc::channel::(8); - let worker = WorkerConnection::new(tx); + + let (registrator_tx, _registrator_rx) = mpsc::channel::(8); + let registrator = WorkerConnection::new(registrator_tx); let msg = Message::TriggerRegistrationResult { - id: "trigger-1".to_string(), - trigger_type: "my-type".to_string(), - function_id: "my-func".to_string(), - error: Some(crate::protocol::ErrorBody { - code: "registration_failed".to_string(), - message: "registration failed".to_string(), - stacktrace: None, - }), + id: "ghost".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-x".to_string(), + error: Some(crate::protocol::ErrorBody::new("x", "y")), }; - // Should still succeed (just logs the error) engine - .router_msg(&worker, &msg) + .router_msg(®istrator, &msg) .await - .expect("TriggerRegistrationResult with error should succeed"); + .expect("router_msg should succeed even when the trigger is unknown"); } #[tokio::test] From 7821254ce17ff7339e4036b827aaac6f2a14961e Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 10:56:04 -0300 Subject: [PATCH 03/11] fix(python-sdk): log trigger registration errors received from engine Python SDK ignored inbound TRIGGER_REGISTRATION_RESULT messages. Add a handler in _handle_message that logs the engine's error body via the iii logger at ERROR level when error is present; no-op on success. --- sdk/packages/python/iii/src/iii/iii.py | 17 ++++++ .../tests/test_trigger_registration_error.py | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 sdk/packages/python/iii/tests/test_trigger_registration_error.py diff --git a/sdk/packages/python/iii/src/iii/iii.py b/sdk/packages/python/iii/src/iii/iii.py index 9fd30ca59..212dadc89 100644 --- a/sdk/packages/python/iii/src/iii/iii.py +++ b/sdk/packages/python/iii/src/iii/iii.py @@ -443,6 +443,8 @@ async def _handle_message(self, raw: str | bytes) -> None: ) elif msg_type == MessageType.REGISTER_TRIGGER.value: asyncio.create_task(self._handle_trigger_registration(data)) + elif msg_type == MessageType.TRIGGER_REGISTRATION_RESULT.value: + self._handle_trigger_registration_result(data) elif msg_type == MessageType.WORKER_REGISTERED.value: worker_id = data.get("worker_id", "") self._worker_id = worker_id @@ -706,6 +708,21 @@ async def _handle_trigger_registration(self, data: dict[str, Any]) -> None: } ) + def _handle_trigger_registration_result(self, data: dict[str, Any]) -> None: + error = data.get("error") + if not error: + return + + trigger_id = data.get("id", "") + trigger_type = data.get("trigger_type") or data.get("type") or "" + message = error.get("message", "") + log.error( + "[iii] Trigger registration failed for %r (%s): %s", + trigger_id, + trigger_type, + message, + ) + # Connection state management def _set_connection_state(self, state: IIIConnectionState) -> None: diff --git a/sdk/packages/python/iii/tests/test_trigger_registration_error.py b/sdk/packages/python/iii/tests/test_trigger_registration_error.py new file mode 100644 index 000000000..bc591b57b --- /dev/null +++ b/sdk/packages/python/iii/tests/test_trigger_registration_error.py @@ -0,0 +1,57 @@ +"""Tests for engine-reported trigger registration errors.""" + +import json + +from unittest.mock import AsyncMock, patch + +from iii.iii import III, InitOptions + + +def _send_message(client: III, payload: dict) -> None: + with patch.object(client, "_send", new_callable=AsyncMock): + client._run_on_loop(client._handle_message(json.dumps(payload))) + + +def test_trigger_registration_result_error_is_logged(caplog): + client = III(address="ws://localhost:9999", options=InitOptions(worker_name="test")) + caplog.set_level("ERROR", logger="iii") + + _send_message( + client, + { + "type": "triggerregistrationresult", + "id": "trig-1", + "trigger_type": "http", + "function_id": "fn-1", + "error": { + "code": "trigger_type_not_found", + "message": 'Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http', + }, + }, + ) + + messages = [record.getMessage() for record in caplog.records] + assert any("iii worker add iii-http" in m for m in messages), messages + assert any("trig-1" in m for m in messages), messages + + client.shutdown() + + +def test_trigger_registration_result_success_does_not_log(caplog): + client = III(address="ws://localhost:9999", options=InitOptions(worker_name="test")) + caplog.set_level("ERROR", logger="iii") + + _send_message( + client, + { + "type": "triggerregistrationresult", + "id": "trig-2", + "trigger_type": "http", + "function_id": "fn-2", + }, + ) + + messages = [record.getMessage() for record in caplog.records] + assert not any("Trigger registration" in m for m in messages), messages + + client.shutdown() From 45f76c4f32c1bd81dad3e55c1d301d47800924c6 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 11:02:47 -0300 Subject: [PATCH 04/11] fix(rust-sdk): log trigger registration errors received from engine Rust SDK ignored inbound Message::TriggerRegistrationResult. Add an arm in handle_message that logs via tracing::error! when error is populated, no-op on success. Pulls in tracing-test for log capture in unit tests. --- Cargo.lock | 22 +++++++++++++ sdk/packages/rust/iii/Cargo.toml | 1 + sdk/packages/rust/iii/src/iii.rs | 56 ++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 7b8d1e64c..a3a643fb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2778,6 +2778,7 @@ dependencies = [ "tokio", "tokio-tungstenite 0.28.0", "tracing", + "tracing-test", "uuid", ] @@ -6474,6 +6475,27 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-test" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/sdk/packages/rust/iii/Cargo.toml b/sdk/packages/rust/iii/Cargo.toml index 15b90236b..4035bb051 100644 --- a/sdk/packages/rust/iii/Cargo.toml +++ b/sdk/packages/rust/iii/Cargo.toml @@ -46,3 +46,4 @@ opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "trace", "testin reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "multipart", "stream"] } serial_test = "3" tokio = { version = "1", features = ["test-util"] } +tracing-test = "0.2" diff --git a/sdk/packages/rust/iii/src/iii.rs b/sdk/packages/rust/iii/src/iii.rs index 623e6acc6..1d857c4bc 100644 --- a/sdk/packages/rust/iii/src/iii.rs +++ b/sdk/packages/rust/iii/src/iii.rs @@ -1571,6 +1571,23 @@ impl III { Message::WorkerRegistered { worker_id } => { tracing::debug!(worker_id = %worker_id, "Worker registered"); } + Message::TriggerRegistrationResult { + id, + trigger_type, + function_id: _, + error, + } => { + if let Some(err) = error { + tracing::error!( + trigger_id = %id, + trigger_type = %trigger_type, + code = %err.code, + "[iii] Trigger registration failed for {:?}: {}", + id, + err.message + ); + } + } _ => {} } @@ -2244,4 +2261,43 @@ mod tests { assert!(!shutdown); assert!(queue.is_empty()); } + + #[tokio::test] + #[tracing_test::traced_test] + async fn trigger_registration_result_error_is_logged() { + let iii = register_worker("ws://localhost:1234", InitOptions::default()); + let payload = serde_json::json!({ + "type": "triggerregistrationresult", + "id": "trig-1", + "trigger_type": "http", + "function_id": "fn-1", + "error": { + "code": "trigger_type_not_found", + "message": "Trigger type \"http\" not found — worker iii-http is missing. Run: iii worker add iii-http", + }, + }) + .to_string(); + + iii.handle_message(&payload).unwrap(); + + assert!(logs_contain("iii worker add iii-http")); + assert!(logs_contain("trig-1")); + } + + #[tokio::test] + #[tracing_test::traced_test] + async fn trigger_registration_result_success_does_not_log_error() { + let iii = register_worker("ws://localhost:1234", InitOptions::default()); + let payload = serde_json::json!({ + "type": "triggerregistrationresult", + "id": "trig-2", + "trigger_type": "http", + "function_id": "fn-2", + }) + .to_string(); + + iii.handle_message(&payload).unwrap(); + + assert!(!logs_contain("Trigger registration failed")); + } } From 626c2389e06db2aeef7260ca3e29919123a6ffe5 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 11:07:16 -0300 Subject: [PATCH 05/11] fix(node-sdk): log trigger registration errors received from engine Node SDK ignored inbound TriggerRegistrationResult. Add an onMessage branch that routes to a new handler logging via console.error when error is populated, no-op on success. Tightens TriggerRegistrationResultMessage.error to ErrorBody and drops the unused result field. --- sdk/packages/node/iii/src/iii-types.ts | 9 ++- sdk/packages/node/iii/src/iii.ts | 14 ++++ .../tests/trigger-registration-error.test.ts | 78 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 sdk/packages/node/iii/tests/trigger-registration-error.test.ts diff --git a/sdk/packages/node/iii/src/iii-types.ts b/sdk/packages/node/iii/src/iii-types.ts index 76a797b4f..4fafc95d4 100644 --- a/sdk/packages/node/iii/src/iii-types.ts +++ b/sdk/packages/node/iii/src/iii-types.ts @@ -29,13 +29,18 @@ export type UnregisterTriggerMessage = { type?: string } +export type ErrorBody = { + code: string + message: string + stacktrace?: string +} + export type TriggerRegistrationResultMessage = { message_type: MessageType.TriggerRegistrationResult id: string type: string function_id: string - result?: unknown - error?: unknown + error?: ErrorBody } export type RegisterTriggerMessage = { diff --git a/sdk/packages/node/iii/src/iii.ts b/sdk/packages/node/iii/src/iii.ts index 1c8178d82..1fcbee456 100644 --- a/sdk/packages/node/iii/src/iii.ts +++ b/sdk/packages/node/iii/src/iii.ts @@ -981,6 +981,16 @@ class Sdk implements ISdk { } } + private onTriggerRegistrationResult( + message: { id: string; trigger_type?: string; type?: string; function_id: string; error?: { code: string; message: string; stacktrace?: string } }, + ): void { + if (!message.error) return + const triggerType = message.trigger_type ?? message.type ?? '' + console.error( + `[iii] Trigger registration failed for "${message.id}" (${triggerType}): ${message.error.message}`, + ) + } + private onMessage(socketMessage: Data): void { let msgType: MessageType let message: Record @@ -1003,6 +1013,10 @@ class Sdk implements ISdk { this.onInvokeFunction(invocation_id, function_id, data, traceparent, baggage) } else if (msgType === MessageType.RegisterTrigger) { this.onRegisterTrigger(message as { trigger_type: string; id: string; function_id: string; config: unknown; metadata?: Record }) + } else if (msgType === MessageType.TriggerRegistrationResult) { + this.onTriggerRegistrationResult( + message as { id: string; trigger_type?: string; type?: string; function_id: string; error?: { code: string; message: string; stacktrace?: string } }, + ) } else if (msgType === MessageType.WorkerRegistered) { const { worker_id } = message as WorkerRegisteredMessage this.workerId = worker_id diff --git a/sdk/packages/node/iii/tests/trigger-registration-error.test.ts b/sdk/packages/node/iii/tests/trigger-registration-error.test.ts new file mode 100644 index 000000000..699d782a4 --- /dev/null +++ b/sdk/packages/node/iii/tests/trigger-registration-error.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { WebSocketServer, type WebSocket } from 'ws' +import { registerWorker } from '../src/iii' +import type { ISdk } from '../src/iii-types' + +describe('trigger registration error surfacing', () => { + let wss: WebSocketServer + let url: string + let sdk: ISdk | undefined + let serverSocket: WebSocket | undefined + + beforeEach(async () => { + wss = new WebSocketServer({ port: 0 }) + await new Promise((resolve) => wss.once('listening', () => resolve())) + const address = wss.address() as { port: number } + url = `ws://127.0.0.1:${address.port}` + serverSocket = undefined + wss.on('connection', (ws) => { + serverSocket = ws + ws.send(JSON.stringify({ type: 'workerregistered', worker_id: 'test-worker' })) + }) + }) + + afterEach(async () => { + sdk?.shutdown?.() + await new Promise((resolve) => wss.close(() => resolve())) + }) + + it('logs to console.error on TriggerRegistrationResult with error', async () => { + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) + sdk = registerWorker(url) + await new Promise((r) => setTimeout(r, 50)) + + serverSocket!.send( + JSON.stringify({ + type: 'triggerregistrationresult', + id: 'trig-1', + trigger_type: 'http', + function_id: 'fn-1', + error: { + code: 'trigger_type_not_found', + message: + 'Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http', + }, + }), + ) + + await new Promise((r) => setTimeout(r, 20)) + expect(spy).toHaveBeenCalled() + const formatted = spy.mock.calls.map((args) => args.join(' ')).join('\n') + expect(formatted).toContain('trig-1') + expect(formatted).toContain('http') + expect(formatted).toContain('iii worker add iii-http') + spy.mockRestore() + }) + + it('does not log on TriggerRegistrationResult success (no error field)', async () => { + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) + sdk = registerWorker(url) + await new Promise((r) => setTimeout(r, 50)) + + serverSocket!.send( + JSON.stringify({ + type: 'triggerregistrationresult', + id: 'trig-2', + trigger_type: 'http', + function_id: 'fn-2', + }), + ) + + await new Promise((r) => setTimeout(r, 20)) + const registrationLogs = spy.mock.calls + .map((args) => args.join(' ')) + .filter((msg) => msg.includes('Trigger registration')) + expect(registrationLogs).toEqual([]) + spy.mockRestore() + }) +}) From ace87fe7a31bcda9f0799f88846ad16ea2365299 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 11:08:31 -0300 Subject: [PATCH 06/11] docs: document trigger registration error logging --- docs/0-11-0/architecture/trigger-types.mdx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/0-11-0/architecture/trigger-types.mdx b/docs/0-11-0/architecture/trigger-types.mdx index 2f7170c67..89200fad7 100644 --- a/docs/0-11-0/architecture/trigger-types.mdx +++ b/docs/0-11-0/architecture/trigger-types.mdx @@ -48,6 +48,24 @@ iii.register_trigger(RegisterTriggerInput { trigger_type: "http".into(), functio +## Registration errors + +When the engine cannot register a trigger — most commonly because the trigger type's worker is not active in the project — it sends a `TriggerRegistrationResult` with an `error` body back to the worker that initiated the request. The SDK logs the error at `ERROR` level so the user sees it during development. + +For built-in trigger types (`http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, `log`), the error message includes the install command for the missing worker. + +Example log line when `iii-http` is not active: + +``` +[iii] Trigger registration failed for "1c1b…" (http): Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http +``` + +Logging targets per SDK: + +- **Node** — `console.error` +- **Python** — the `iii` logger, level `ERROR` +- **Rust** — `tracing::error!` + ## Trigger Pipeline ```mermaid From 834cf3aab91fa4f7327a4bb1ae4a16ab473aebc7 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 15 May 2026 16:49:21 -0300 Subject: [PATCH 07/11] fix(engine): recommend workers directory for unknown non-builtin trigger types When a registered trigger type is neither built-in nor active, the error message now points users to https://workers.iii.dev/ to find a worker that provides it. Built-in types keep their `iii worker add ` hint. --- docs/0-11-0/architecture/trigger-types.mdx | 8 +++- engine/src/engine/mod.rs | 50 +++++++++++++++++++++- engine/src/trigger.rs | 14 +++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/docs/0-11-0/architecture/trigger-types.mdx b/docs/0-11-0/architecture/trigger-types.mdx index 89200fad7..efececea6 100644 --- a/docs/0-11-0/architecture/trigger-types.mdx +++ b/docs/0-11-0/architecture/trigger-types.mdx @@ -52,7 +52,7 @@ iii.register_trigger(RegisterTriggerInput { trigger_type: "http".into(), functio When the engine cannot register a trigger — most commonly because the trigger type's worker is not active in the project — it sends a `TriggerRegistrationResult` with an `error` body back to the worker that initiated the request. The SDK logs the error at `ERROR` level so the user sees it during development. -For built-in trigger types (`http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, `log`), the error message includes the install command for the missing worker. +For built-in trigger types (`http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, `log`), the error message includes the install command for the missing worker. For unknown trigger types that are not built-in, the error message points to the workers directory at . Example log line when `iii-http` is not active: @@ -60,6 +60,12 @@ Example log line when `iii-http` is not active: [iii] Trigger registration failed for "1c1b…" (http): Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http ``` +Example log line for an unknown non-built-in trigger type: + +``` +[iii] Trigger registration failed for "1c1b…" (my-custom-trigger): Trigger type "my-custom-trigger" not found. Search for a worker that provides this trigger type at https://workers.iii.dev/ +``` + Logging targets per SDK: - **Node** — `console.error` diff --git a/engine/src/engine/mod.rs b/engine/src/engine/mod.rs index 85cd6cdd0..5740dbe0b 100644 --- a/engine/src/engine/mod.rs +++ b/engine/src/engine/mod.rs @@ -642,6 +642,49 @@ impl Engine { return Ok(()); }; + let forward = Message::TriggerRegistrationResult { + id: id.clone(), + trigger_type: trigger_type.clone(), + function_id: function_id.clone(), + error: error.clone(), + }; + let _ = self.send_msg(&originator, forward).await; + + let Some(trigger_entry) = self.trigger_registry.triggers.get(id) else { + tracing::debug!( + trigger_id = %id, + "TriggerRegistrationResult for unknown trigger; ignoring" + ); + return Ok(()); + }; + let originator_id = trigger_entry.worker_id; + drop(trigger_entry); + + if error.is_some() { + self.trigger_registry.triggers.remove(id); + } + + if error.is_none() { + return Ok(()); + } + + let Some(originator_id) = originator_id else { + tracing::debug!( + trigger_id = %id, + "TriggerRegistrationResult for trigger without originator; ignoring" + ); + return Ok(()); + }; + + let Some(originator) = self.worker_registry.get_worker(&originator_id) else { + tracing::debug!( + trigger_id = %id, + originator = %originator_id, + "TriggerRegistrationResult originator no longer connected; dropping" + ); + return Ok(()); + }; + let forward = Message::TriggerRegistrationResult { id: id.clone(), trigger_type: trigger_type.clone(), @@ -3468,7 +3511,7 @@ mod tests { } #[tokio::test] - async fn test_register_trigger_unknown_type_sends_generic_error() { + async fn test_register_trigger_unknown_type_recommends_workers_directory() { ensure_default_meter(); let engine = Engine::new(); let (tx, mut rx) = mpsc::channel::(8); @@ -3498,6 +3541,11 @@ mod tests { "msg should name the missing type: {}", err.message ); + assert!( + err.message.contains("https://workers.iii.dev/"), + "msg should recommend the workers directory: {}", + err.message + ); } // ========================================================================= diff --git a/engine/src/trigger.rs b/engine/src/trigger.rs index c4679151e..5e54c9f91 100644 --- a/engine/src/trigger.rs +++ b/engine/src/trigger.rs @@ -41,7 +41,9 @@ pub enum RegisterTriggerError { trigger_type: String, worker: &'static str, }, - #[error("Trigger type \"{trigger_type}\" not found")] + #[error( + "Trigger type \"{trigger_type}\" not found. Search for a worker that provides this trigger type at https://workers.iii.dev/" + )] Unknown { trigger_type: String }, #[error(transparent)] Other(#[from] anyhow::Error), @@ -262,7 +264,11 @@ impl TriggerRegistry { }); } - tracing::error!("Trigger type {} not found", trigger_type_id.purple()); + tracing::error!( + "Trigger type {} not found. Search for a worker that provides this trigger type at {}", + trigger_type_id.purple().bold(), + "https://workers.iii.dev/".cyan().bold() + ); return Err(RegisterTriggerError::Unknown { trigger_type: trigger_type_id, }); @@ -492,6 +498,10 @@ mod tests { err_msg.contains("\"nonexistent\" not found"), "Expected unknown-type message, got: {err_msg}" ); + assert!( + err_msg.contains("https://workers.iii.dev/"), + "Expected workers directory recommendation, got: {err_msg}" + ); assert!(registry.triggers.is_empty()); } From 0ee2dc1e08e5a1340949f983bbbb6818de349e02 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Mon, 18 May 2026 09:09:39 -0300 Subject: [PATCH 08/11] fix(sdk): correct ISdk import + collapse clippy match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Node: ISdk lives in ./types, not ./iii-types — fix test import so `tsc --noEmit` passes. - Rust: collapse `if let Some(err) = error` into outer match's `Some(err)` binding per `clippy::collapsible_match`. --- .../tests/trigger-registration-error.test.ts | 2 +- sdk/packages/rust/iii/src/iii.rs | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/sdk/packages/node/iii/tests/trigger-registration-error.test.ts b/sdk/packages/node/iii/tests/trigger-registration-error.test.ts index 699d782a4..257025b95 100644 --- a/sdk/packages/node/iii/tests/trigger-registration-error.test.ts +++ b/sdk/packages/node/iii/tests/trigger-registration-error.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { WebSocketServer, type WebSocket } from 'ws' import { registerWorker } from '../src/iii' -import type { ISdk } from '../src/iii-types' +import type { ISdk } from '../src/types' describe('trigger registration error surfacing', () => { let wss: WebSocketServer diff --git a/sdk/packages/rust/iii/src/iii.rs b/sdk/packages/rust/iii/src/iii.rs index 1d857c4bc..2cb776290 100644 --- a/sdk/packages/rust/iii/src/iii.rs +++ b/sdk/packages/rust/iii/src/iii.rs @@ -1575,18 +1575,16 @@ impl III { id, trigger_type, function_id: _, - error, + error: Some(err), } => { - if let Some(err) = error { - tracing::error!( - trigger_id = %id, - trigger_type = %trigger_type, - code = %err.code, - "[iii] Trigger registration failed for {:?}: {}", - id, - err.message - ); - } + tracing::error!( + trigger_id = %id, + trigger_type = %trigger_type, + code = %err.code, + "[iii] Trigger registration failed for {:?}: {}", + id, + err.message + ); } _ => {} } From daabdae4881edf28227aafad27247389d344436d Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Mon, 18 May 2026 09:39:11 -0300 Subject: [PATCH 09/11] fix: address CodeRabbit review feedback - engine/path B: validate that TriggerRegistrationResult is sent by the registrator worker that owns the trigger_type. Reject and ignore reports from non-registrators so a buggy/compromised worker cannot spoof failures and tear other workers' triggers out of the registry. Forwarded message is now built from the canonical stored trigger (id, trigger_type, function_id) rather than the inbound payload. - engine/path A: only increment `track_trigger_registered` on successful registration; failed registrations were skewing the metric. - docs: align built-in trigger type list with the engine's BUILTIN_TRIGGER_TYPES const (adds stream:join / stream:leave and notes that the engine reports the registered name verbatim). - node test: await `sdk.shutdown()` and restore mocks in teardown so the WebSocket server close does not race with pending SDK work. --- docs/0-11-0/architecture/trigger-types.mdx | 2 +- engine/src/engine/mod.rs | 138 ++++++++++++------ .../tests/trigger-registration-error.test.ts | 5 +- 3 files changed, 97 insertions(+), 48 deletions(-) diff --git a/docs/0-11-0/architecture/trigger-types.mdx b/docs/0-11-0/architecture/trigger-types.mdx index efececea6..52f2520fb 100644 --- a/docs/0-11-0/architecture/trigger-types.mdx +++ b/docs/0-11-0/architecture/trigger-types.mdx @@ -52,7 +52,7 @@ iii.register_trigger(RegisterTriggerInput { trigger_type: "http".into(), functio When the engine cannot register a trigger — most commonly because the trigger type's worker is not active in the project — it sends a `TriggerRegistrationResult` with an `error` body back to the worker that initiated the request. The SDK logs the error at `ERROR` level so the user sees it during development. -For built-in trigger types (`http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, `log`), the error message includes the install command for the missing worker. For unknown trigger types that are not built-in, the error message points to the workers directory at . +For built-in trigger types — `http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, `stream:join`, `stream:leave`, and `log` — the error message includes the install command for the missing worker (the exact name reported matches what the worker registers, e.g. `stream:join` rather than the generic `stream`). For unknown trigger types that are not built-in, the error message points to the workers directory at . Example log line when `iii-http` is not active: diff --git a/engine/src/engine/mod.rs b/engine/src/engine/mod.rs index 5740dbe0b..cbc8055ce 100644 --- a/engine/src/engine/mod.rs +++ b/engine/src/engine/mod.rs @@ -614,60 +614,37 @@ impl Engine { ); return Ok(()); }; + let stored_trigger_type = trigger_entry.trigger_type.clone(); + let stored_function_id = trigger_entry.function_id.clone(); let originator_id = trigger_entry.worker_id; drop(trigger_entry); - if error.is_some() { - self.trigger_registry.triggers.remove(id); - } - - if error.is_none() { - return Ok(()); - } - - let Some(originator_id) = originator_id else { - tracing::debug!( - trigger_id = %id, - "TriggerRegistrationResult for trigger without originator; ignoring" - ); - return Ok(()); - }; - - let Some(originator) = self.worker_registry.get_worker(&originator_id) else { - tracing::debug!( - trigger_id = %id, - originator = %originator_id, - "TriggerRegistrationResult originator no longer connected; dropping" - ); - return Ok(()); - }; - - let forward = Message::TriggerRegistrationResult { - id: id.clone(), - trigger_type: trigger_type.clone(), - function_id: function_id.clone(), - error: error.clone(), - }; - let _ = self.send_msg(&originator, forward).await; - - let Some(trigger_entry) = self.trigger_registry.triggers.get(id) else { - tracing::debug!( + // Only the registrator worker that owns this trigger_type may + // report its result. Otherwise any connected worker could spoof + // a failure for somebody else's trigger and tear it out of the + // registry. + let registrator_worker_id = self + .trigger_registry + .trigger_types + .get(&stored_trigger_type) + .and_then(|tt| tt.worker_id); + if registrator_worker_id != Some(worker.id) { + tracing::warn!( trigger_id = %id, - "TriggerRegistrationResult for unknown trigger; ignoring" + trigger_type = %stored_trigger_type, + sender = %worker.id, + registrator = ?registrator_worker_id, + "TriggerRegistrationResult from non-registrator worker; ignoring" ); return Ok(()); - }; - let originator_id = trigger_entry.worker_id; - drop(trigger_entry); - - if error.is_some() { - self.trigger_registry.triggers.remove(id); } if error.is_none() { return Ok(()); } + self.trigger_registry.triggers.remove(id); + let Some(originator_id) = originator_id else { tracing::debug!( trigger_id = %id, @@ -687,8 +664,8 @@ impl Engine { let forward = Message::TriggerRegistrationResult { id: id.clone(), - trigger_type: trigger_type.clone(), - function_id: function_id.clone(), + trigger_type: stored_trigger_type, + function_id: stored_function_id, error: error.clone(), }; let _ = self.send_msg(&originator, forward).await; @@ -868,7 +845,9 @@ impl Engine { }) .await { - Ok(()) => {} + Ok(()) => { + crate::workers::telemetry::collector::track_trigger_registered(); + } Err(err) => { let error_body = match &err { crate::trigger::RegisterTriggerError::UnknownBuiltin { .. } @@ -894,7 +873,6 @@ impl Engine { let _ = self.send_msg(worker, result_msg).await; } } - crate::workers::telemetry::collector::track_trigger_registered(); Ok(()) } @@ -3333,6 +3311,18 @@ mod tests { ); } + fn insert_trigger_type_for(engine: &Engine, type_id: &str, registrator: &WorkerConnection) { + engine.trigger_registry.trigger_types.insert( + type_id.to_string(), + crate::trigger::TriggerType::new( + type_id, + "test trigger type", + Box::new(registrator.clone()), + Some(registrator.id), + ), + ); + } + #[tokio::test] async fn test_trigger_registration_result_forwards_error_to_originator() { ensure_default_meter(); @@ -3345,6 +3335,8 @@ mod tests { let (registrator_tx, _registrator_rx) = mpsc::channel::(8); let registrator = WorkerConnection::new(registrator_tx); + insert_trigger_type_for(&engine, "http", ®istrator); + engine.trigger_registry.triggers.insert( "trig-1".to_string(), crate::trigger::Trigger { @@ -3409,6 +3401,8 @@ mod tests { let (registrator_tx, _registrator_rx) = mpsc::channel::(8); let registrator = WorkerConnection::new(registrator_tx); + insert_trigger_type_for(&engine, "http", ®istrator); + engine.trigger_registry.triggers.insert( "trig-2".to_string(), crate::trigger::Trigger { @@ -3465,6 +3459,58 @@ mod tests { .expect("router_msg should succeed even when the trigger is unknown"); } + #[tokio::test] + async fn test_trigger_registration_result_from_non_registrator_is_ignored() { + ensure_default_meter(); + let engine = Engine::new(); + + let (user_tx, mut user_rx) = mpsc::channel::(8); + let user = WorkerConnection::new(user_tx); + engine.worker_registry.register_worker(user.clone()); + + // Registered registrator for "http". + let (registrator_tx, _registrator_rx) = mpsc::channel::(8); + let registrator = WorkerConnection::new(registrator_tx); + insert_trigger_type_for(&engine, "http", ®istrator); + + engine.trigger_registry.triggers.insert( + "trig-3".to_string(), + crate::trigger::Trigger { + id: "trig-3".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-3".to_string(), + config: serde_json::json!({}), + worker_id: Some(user.id), + metadata: None, + }, + ); + + // Some OTHER worker tries to report a failure for trig-3. + let (spoofer_tx, _spoofer_rx) = mpsc::channel::(8); + let spoofer = WorkerConnection::new(spoofer_tx); + + let msg = Message::TriggerRegistrationResult { + id: "trig-3".to_string(), + trigger_type: "http".to_string(), + function_id: "fn-3".to_string(), + error: Some(crate::protocol::ErrorBody::new("spoofed", "boom")), + }; + + engine + .router_msg(&spoofer, &msg) + .await + .expect("router_msg should succeed"); + + assert!( + user_rx.try_recv().is_err(), + "non-registrator result must not be forwarded" + ); + assert!( + engine.trigger_registry.triggers.get("trig-3").is_some(), + "non-registrator result must not remove the trigger" + ); + } + #[tokio::test] async fn test_register_trigger_unknown_builtin_sends_install_hint() { ensure_default_meter(); diff --git a/sdk/packages/node/iii/tests/trigger-registration-error.test.ts b/sdk/packages/node/iii/tests/trigger-registration-error.test.ts index 257025b95..36655859c 100644 --- a/sdk/packages/node/iii/tests/trigger-registration-error.test.ts +++ b/sdk/packages/node/iii/tests/trigger-registration-error.test.ts @@ -22,7 +22,10 @@ describe('trigger registration error surfacing', () => { }) afterEach(async () => { - sdk?.shutdown?.() + if (sdk) { + await sdk.shutdown() + } + vi.restoreAllMocks() await new Promise((resolve) => wss.close(() => resolve())) }) From f140f1d60712ddbff6434114e04ede3b4964f450 Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Fri, 22 May 2026 14:35:46 -0300 Subject: [PATCH 10/11] fix(sdk-python): drop misleading message-type fallback in trigger registration error log Use data.get("trigger_type", "") instead of falling back to data.get("type"), which returned the WS message type ("triggerregistrationresult") and produced confusing error logs when trigger_type was absent. --- sdk/packages/python/iii/src/iii/iii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/packages/python/iii/src/iii/iii.py b/sdk/packages/python/iii/src/iii/iii.py index 9d81e60ec..95f8297de 100644 --- a/sdk/packages/python/iii/src/iii/iii.py +++ b/sdk/packages/python/iii/src/iii/iii.py @@ -709,7 +709,7 @@ def _handle_trigger_registration_result(self, data: dict[str, Any]) -> None: return trigger_id = data.get("id", "") - trigger_type = data.get("trigger_type") or data.get("type") or "" + trigger_type = data.get("trigger_type", "") message = error.get("message", "") log.error( "[iii] Trigger registration failed for %r (%s): %s", From c51659c9dc3c84b50392df336ff6c68ed8cde19d Mon Sep 17 00:00:00 2001 From: Guilherme Beira Date: Mon, 25 May 2026 14:05:49 -0300 Subject: [PATCH 11/11] docs: move trigger registration errors section to 0.13 --- docs/0-11-0/architecture/trigger-types.mdx | 24 ----------------- docs/understanding-iii/triggers.mdx | 31 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/0-11-0/architecture/trigger-types.mdx b/docs/0-11-0/architecture/trigger-types.mdx index 52f2520fb..2f7170c67 100644 --- a/docs/0-11-0/architecture/trigger-types.mdx +++ b/docs/0-11-0/architecture/trigger-types.mdx @@ -48,30 +48,6 @@ iii.register_trigger(RegisterTriggerInput { trigger_type: "http".into(), functio -## Registration errors - -When the engine cannot register a trigger — most commonly because the trigger type's worker is not active in the project — it sends a `TriggerRegistrationResult` with an `error` body back to the worker that initiated the request. The SDK logs the error at `ERROR` level so the user sees it during development. - -For built-in trigger types — `http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, `stream:join`, `stream:leave`, and `log` — the error message includes the install command for the missing worker (the exact name reported matches what the worker registers, e.g. `stream:join` rather than the generic `stream`). For unknown trigger types that are not built-in, the error message points to the workers directory at . - -Example log line when `iii-http` is not active: - -``` -[iii] Trigger registration failed for "1c1b…" (http): Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http -``` - -Example log line for an unknown non-built-in trigger type: - -``` -[iii] Trigger registration failed for "1c1b…" (my-custom-trigger): Trigger type "my-custom-trigger" not found. Search for a worker that provides this trigger type at https://workers.iii.dev/ -``` - -Logging targets per SDK: - -- **Node** — `console.error` -- **Python** — the `iii` logger, level `ERROR` -- **Rust** — `tracing::error!` - ## Trigger Pipeline ```mermaid diff --git a/docs/understanding-iii/triggers.mdx b/docs/understanding-iii/triggers.mdx index 7bc2d968a..d19564246 100644 --- a/docs/understanding-iii/triggers.mdx +++ b/docs/understanding-iii/triggers.mdx @@ -82,3 +82,34 @@ Trigger fires, the Engine invokes the condition function with the same payload t receive. If the condition returns a truthy value, the handler runs; if not, the invocation is skipped. Use this when the same event source should sometimes fire the Function and sometimes not, without splitting the Trigger into separate event-source registrations. + +## Registration errors + +When the Engine cannot register a Trigger — most commonly because the Trigger type's Worker is not +active in the project — it sends a `TriggerRegistrationResult` with an `error` body back to the +Worker that initiated the request. The SDK logs the error at `ERROR` level so the user sees it +during development. + +For built-in Trigger types — `http`, `cron`, `subscribe`, `state`, `durable:subscriber`, `stream`, +`stream:join`, `stream:leave`, and `log` — the error message includes the install command for the +missing Worker (the exact name reported matches what the Worker registers, e.g. `stream:join` +rather than the generic `stream`). For unknown Trigger types that are not built-in, the error +message points to the workers directory at . + +Example log line when `iii-http` is not active: + +``` +[iii] Trigger registration failed for "1c1b…" (http): Trigger type "http" not found — worker iii-http is missing. Run: iii worker add iii-http +``` + +Example log line for an unknown non-built-in Trigger type: + +``` +[iii] Trigger registration failed for "1c1b…" (my-custom-trigger): Trigger type "my-custom-trigger" not found. Search for a worker that provides this trigger type at https://workers.iii.dev/ +``` + +Logging targets per SDK: + +- **Node** — `console.error` +- **Python** — the `iii` logger, level `ERROR` +- **Rust** — `tracing::error!`