feat(automations): event trigger support with callback system and runtime SDK#2844
feat(automations): event trigger support with callback system and runtime SDK#2844viktormarinho merged 16 commits intomainfrom
Conversation
… runtime SDK Add end-to-end event-based trigger support for automations: - UI: event trigger form with connection selector, event type picker, and params - Backend: callback token system (SHA-256 hashed) for secure MCP-to-Mesh delivery - API: POST /api/trigger-callback endpoint for external MCPs to fire automations - Binding: callbackUrl/callbackToken fields on TRIGGER_CONFIGURE schema - Runtime: createTriggers() SDK helper so MCPs can implement triggers declaratively Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsSuggested: Minor ( React with an emoji to override the release type:
Current version:
|
There was a problem hiding this comment.
8 issues found across 21 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/runtime/src/triggers.ts">
<violation number="1" location="packages/runtime/src/triggers.ts:84">
P2: Include enum values from z.enum parameters when building paramsSchema so trigger options aren’t dropped.</violation>
<violation number="2" location="packages/runtime/src/triggers.ts:128">
P2: Don’t delete connection-level callback credentials when disabling a single trigger type, or other enabled trigger types will stop delivering events.</violation>
</file>
<file name="apps/mesh/src/api/routes/trigger-callback.ts">
<violation number="1" location="apps/mesh/src/api/routes/trigger-callback.ts:40">
P2: The `content-length` header check is bypassable because the header is client-controlled and can be omitted or spoofed. Use Hono's `bodyLimit` middleware instead, which enforces the limit at the stream level before the body is fully read.</violation>
</file>
<file name="packages/runtime/src/triggers.test.ts">
<violation number="1" location="packages/runtime/src/triggers.test.ts:79">
P2: Use a short fixed delay around 50ms for the fire-and-forget flush; 10ms can be too short and lead to flaky tests.
(Based on your team's feedback about allowing a ~50ms delay to flush fire-and-forget async work in tests.) [FEEDBACK_USED]</violation>
</file>
<file name="apps/mesh/src/storage/trigger-callback-tokens.ts">
<violation number="1" location="apps/mesh/src/storage/trigger-callback-tokens.ts:68">
P1: Replace the non-atomic DELETE + INSERT with a single `onConflict` upsert to avoid a race condition under concurrent token rotation for the same connection. Two concurrent calls can both complete the DELETE, then the second INSERT fails on the unique index.
(Based on your team's feedback about using atomic upserts instead of pre-check + create patterns.) [FEEDBACK_USED]</violation>
</file>
<file name="packages/bindings/src/well-known/trigger.ts">
<violation number="1" location="packages/bindings/src/well-known/trigger.ts:71">
P2: Validate `callbackUrl` as a URL instead of accepting arbitrary strings.</violation>
<violation number="2" location="packages/bindings/src/well-known/trigger.ts:72">
P2: Reject empty `callbackToken` values when the token is provided.</violation>
</file>
<file name="apps/mesh/migrations/052-trigger-callback-tokens.ts">
<violation number="1" location="apps/mesh/migrations/052-trigger-callback-tokens.ts:11">
P1: This migration is not registered in `migrations/index.ts`, so it will never run and the new table won't exist.
(Based on your team's feedback about verifying migration registration in migrations/index.ts.) [FEEDBACK_USED]</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
- Extract enum values from z.enum() params in createTriggers paramsSchema - Don't delete connection-level callback credentials on single trigger disable - Use Hono bodyLimit middleware instead of client-controlled content-length check - Atomic upsert (onConflict) for token rotation instead of DELETE+INSERT - Validate callbackUrl as URL, reject empty callbackToken in binding schema - Register migration 052 in migrations/index.ts - Bump test delays to 50ms to avoid flaky fire-and-forget assertions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/runtime/src/triggers.ts">
<violation number="1" location="packages/runtime/src/triggers.ts:131">
P1: Missing credential cleanup on reconfigure/disable leaves stale callback tokens active, so `notify()` can continue firing events after a trigger should be inactive.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Main added 052-thread-agent-ids, our branch had 052-trigger-callback-tokens. Renumbered ours to 053-trigger-callback-tokens to preserve both. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e953300 to
854bdc2
Compare
- Migrations: renumber 053→058 to follow main's 053-057 additions - use-automations: merge isError check with main's automationsAll query key - automation-detail: port event trigger UI (EventTriggerForm, TriggerCard params/connection display, AddStarterPopover event option) to new views/automations/ and components/automations/ structure after main's sidebar spaces refactor extracted and relocated the components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…xx callbacks 1. configure-trigger: generate token pair without persisting, call TRIGGER_CONFIGURE, then persist hash only on success. On disable, delete token only after MCP confirms. Prevents zombie tokens on failure in either direction. 2. trigger-callback-tokens: split createOrRotateToken into generateTokenPair() + persistTokenHash() for two-phase creation. 3. runtime notify(): check response.ok and log non-2xx status codes instead of silently swallowing them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/tools/automations/configure-trigger.ts">
<violation number="1" location="apps/mesh/src/tools/automations/configure-trigger.ts:65">
P1: Persisting the token hash only after `TRIGGER_CONFIGURE` can leave MCP and Mesh out of sync on timeout: the MCP may accept the token while Mesh never stores it.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
- Wrap EventTriggerForm in Suspense boundary so useConnections({ binding: "TRIGGER" })
doesn't suspend the entire automation detail page
- Show inline loading spinner inside the event type select instead of
disabling the whole select
- Move useConnections out of TriggerCard into SettingsTab and pass
connectionName as a prop to avoid per-card suspense
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Timeout is ambiguous — the MCP may still accept the token after Promise.race gives up. Persist the hash on timeout so future callbacks can authenticate. On definitive (non-timeout) rejection, skip persistence since the MCP never received the token and the old one (if any) remains valid. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…bled Track active trigger types per connection. When a trigger is disabled, remove it from the set. When the set is empty (no more active triggers), delete the callback credentials so notify() stops sending callbacks. This prevents wasteful POSTs to Mesh after all triggers for a connection are disabled, and avoids noisy 401 errors in logs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add optional `storage` parameter to `createTriggers()` so MCPs can
persist callback credentials and active trigger types across restarts.
API is backwards compatible — array form still works:
createTriggers([...]) // in-memory only
createTriggers({ definitions, storage }) // with persistence
TriggerStorage interface has get/set/delete keyed by connectionId.
On notify(), the SDK checks in-memory cache first, then falls back to
async storage load. State is persisted on every enable/disable and
cleaned up from storage when the last trigger is disabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/runtime/src/triggers.ts">
<violation number="1" location="packages/runtime/src/triggers.ts:107">
P2: Disable doesn’t load persisted trigger state, so after a restart a disable request won’t clear stored credentials. That leaves stale callback credentials in storage and notify() can keep delivering events for a disabled connection.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Mesh side:
- Migration 059: kv table (org_id + key composite PK, JSONB value)
- KyselyKVStorage: get/set/delete with atomic upsert
- HTTP routes: GET/PUT/DELETE /api/kv/:key (auth via API key, org from session)
Runtime side:
- StudioKV: TriggerStorage backed by Mesh's KV API (for production MCPs)
- JsonFileStorage: TriggerStorage backed by a local JSON file (for dev)
- Exported from @decocms/runtime/trigger-storage
Usage:
import { StudioKV } from "@decocms/runtime/trigger-storage";
const triggers = createTriggers({
definitions: [...],
storage: new StudioKV({
url: process.env.MESH_URL,
apiKey: process.env.MESH_API_KEY,
}),
});
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual typeof checks with TriggerCallbackBodySchema.safeParse() per CLAUDE.md convention: "Use Zod for runtime validation." Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…entials After a restart, disable() found nothing in memory and skipped storage cleanup. Now loads from storage first, so disabling a trigger after restart properly clears persisted credentials. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/runtime/src/trigger-storage.ts">
<violation number="1" location="packages/runtime/src/trigger-storage.ts:159">
P1: Do not swallow all file/parse errors in `JsonFileStorage.load()`; only treat missing files as empty state and rethrow other errors.
(Based on your team's feedback about only treating missing files as optional and rethrowing other read/parse errors.) [FEEDBACK_USED]</violation>
</file>
<file name="apps/mesh/src/storage/kv.ts">
<violation number="1" location="apps/mesh/src/storage/kv.ts:52">
P2: `kv.value` is a jsonb column; stringifying here stores a string and `get()` will return the wrong type. Pass the object directly so PostgreSQL stores it as JSON.</violation>
</file>
<file name="apps/mesh/src/api/routes/trigger-callback.ts">
<violation number="1" location="apps/mesh/src/api/routes/trigger-callback.ts:18">
P2: `type` validation now allows empty strings, so blank event types can pass and trigger downstream processing.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| .values({ | ||
| organization_id: organizationId, | ||
| key, | ||
| value: JSON.stringify(value), |
There was a problem hiding this comment.
P2: kv.value is a jsonb column; stringifying here stores a string and get() will return the wrong type. Pass the object directly so PostgreSQL stores it as JSON.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/storage/kv.ts, line 52:
<comment>`kv.value` is a jsonb column; stringifying here stores a string and `get()` will return the wrong type. Pass the object directly so PostgreSQL stores it as JSON.</comment>
<file context>
@@ -0,0 +1,71 @@
+ .values({
+ organization_id: organizationId,
+ key,
+ value: JSON.stringify(value),
+ updated_at: new Date().toISOString(),
+ })
</file context>
…P confirms Once TRIGGER_CONFIGURE succeeds, the MCP is listening. If the subsequent DB write (persistTokenHash/deleteByConnection) throws, log the error but still return success so the caller creates the trigger record. Prevents orphaned triggers on the MCP with no matching record in Mesh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Main removed unused Link import from @tanstack/react-router. Our branch added useConnections import from @decocms/mesh-sdk. Kept both changes: useConnections + removed Link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/tools/automations/configure-trigger.ts">
<violation number="1" location="apps/mesh/src/tools/automations/configure-trigger.ts:99">
P1: Do not swallow token persistence failures after successful TRIGGER_CONFIGURE; returning success here can leave an enabled trigger with unusable callback authentication state.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| } catch (dbErr) { | ||
| console.error( | ||
| `[configureTriggerOnMcp] Token persistence failed after successful TRIGGER_CONFIGURE:`, | ||
| dbErr, | ||
| ); | ||
| } |
There was a problem hiding this comment.
P1: Do not swallow token persistence failures after successful TRIGGER_CONFIGURE; returning success here can leave an enabled trigger with unusable callback authentication state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/tools/automations/configure-trigger.ts, line 99:
<comment>Do not swallow token persistence failures after successful TRIGGER_CONFIGURE; returning success here can leave an enabled trigger with unusable callback authentication state.</comment>
<file context>
@@ -79,18 +79,27 @@ export async function configureTriggerOnMcp(
+ organizationId,
+ );
+ }
+ } catch (dbErr) {
+ console.error(
+ `[configureTriggerOnMcp] Token persistence failed after successful TRIGGER_CONFIGURE:`,
</file context>
| } catch (dbErr) { | |
| console.error( | |
| `[configureTriggerOnMcp] Token persistence failed after successful TRIGGER_CONFIGURE:`, | |
| dbErr, | |
| ); | |
| } | |
| } catch (dbErr) { | |
| console.error( | |
| `[configureTriggerOnMcp] Token persistence failed after successful TRIGGER_CONFIGURE:`, | |
| dbErr, | |
| ); | |
| throw dbErr; | |
| } |
1. JsonFileStorage.load(): only treat ENOENT as empty state, rethrow parse errors and permission failures instead of silently swallowing 2. trigger-callback: reject empty type strings with z.string().min(1) KV stringify issue (#2) is not valid — Kysely+PG requires JSON.stringify for jsonb column inserts, matching the codebase convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
POST /api/trigger-callbackcallbackUrl/callbackTokenoptional fields toTRIGGER_CONFIGUREschemacreateTriggers()helper so MCPs can implement triggers declaratively with Zod params — handles TRIGGER_LIST, TRIGGER_CONFIGURE tools, andnotify()for fire-and-forget delivery to MeshHow it works
TRIGGER_CONFIGUREon the MCP with a callback URL + tokentriggers.notify(connectionId, type, data)/api/trigger-callbackvalidates the token and fires matching automations viaEventTriggerEngineTest plan
createTriggersunit tests (6 tests passing)🤖 Generated with Claude Code
Summary by cubic
Adds end-to-end event triggers for automations with a secure MCP→Mesh callback, a runtime SDK, and an org‑scoped KV store. Hardens validation and storage for more reliable callbacks across restarts.
New Features
TRIGGERconnection, select event type, fill params. Normalizes schemas (incl.z.enum). Cards show event · connection and params. Ported to new views after the spaces refactor./api/trigger-callbackwith Bearer auth, Zod‑validated body, 1MB limit viahono/body-limit. Tokens are SHA‑256 hashed, scoped per connection+org, rotated with atomic upsert. Two‑phase persistence on enable (persist after MCP confirms, or on timeout), and delete on disable after MCP confirms. Returns 202. If DB write fails after MCP confirms, we log and still succeed so trigger creation isn’t blocked.@decocms/runtime/triggerscreateTriggers()exposesTRIGGER_LIST/TRIGGER_CONFIGUREandnotify(). Extracts enum options fromz.enum. OptionalTriggerStoragepersists callback credentials and active trigger types;@decocms/runtime/trigger-storageprovidesStudioKVandJsonFileStorage.notify()logs non‑2xx and loads persisted state on demand after restarts; disable loads before cleanup so stale credentials are removed.kvtable. API GET/PUT/DELETE/api/kv/:keyfor small state. DB migrations 058–059 included.Bug Fixes
JsonFileStorage: only treats ENOENT as empty; rethrows parse/permission errors.typevalues (z.string().min(1)).Written for commit b5d1e5a. Summary will update on new commits.