Skip to content

feat(automations): add webhook trigger type#3426

Merged
tlgimenes merged 7 commits into
mainfrom
viktormarinho/webhook-trigger-automation
May 22, 2026
Merged

feat(automations): add webhook trigger type#3426
tlgimenes merged 7 commits into
mainfrom
viktormarinho/webhook-trigger-automation

Conversation

@viktormarinho
Copy link
Copy Markdown
Contributor

@viktormarinho viktormarinho commented May 21, 2026

What is this contribution about?

Adds a third automation trigger type, webhook, alongside cron and event. Creating one mints a Better Auth API key scoped via { webhook_<trigger_id>: ["FIRE"] } and returns a one-time URL + token; senders POST JSON to /api/:org/webhooks/:triggerId (or /:triggerId/:token) with either Authorization: Bearer <token> or the path-embedded form. A new AUTOMATION_TRIGGER_ROTATE_TOKEN tool rotates the secret (mint new, swap, revoke old), and AUTOMATION_TRIGGER_REMOVE now also deletes the underlying key. The callback route bypasses the buggy event-bus dispatch path and goes straight through enqueueAutomationFire so webhook fires still flow through the same DBOS org/global concurrency gates. UI gains a Webhook option in the starter popover, a one-time secret reveal dialog (URL/token/curl), and a Rotate action on the trigger card.

How to Test

  1. Run bun run --cwd=apps/mesh migrate to apply 082-automation-webhook-triggers.
  2. Open an automation, click Add Starter → Webhook. The reveal dialog should show URL + token; copy them.
  3. curl -X POST '<URL>' -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' -d '{\"hello\":\"world\"}' → expect 202 { ok: true, trigger_id } and the automation to run with the payload in its context.
  4. Try the path-token form curl -X POST '<URL>/<token>' — same result.
  5. Hit Rotate on the trigger card → new token revealed; old token returns 401; new token fires the automation.
  6. Removing the trigger should revoke the key (subsequent POSTs return 401).

Migration Notes

Migration 082-automation-webhook-triggers relaxes the chk_trigger_type constraint to include 'webhook' and adds a nullable api_key_id column to automation_triggers. Safe to roll forward; the down migration restores the old constraint after clearing the new column.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

🤖 Generated with Claude Code


Summary by cubic

Adds a new webhook trigger so external systems can fire automations via HTTP POST with scoped Better Auth tokens; includes DB migration 090-automation-webhook-triggers to allow type: "webhook" and store api_key_id. Also improves background runs by persisting stream errors as assistant messages, fixing desktop sandbox binding for default targets in local dev, and small chat UI polish.

  • New Features

    • API: POST /api/:org/webhooks/:triggerId (Bearer) or POST /api/:org/webhooks/:triggerId/:token; accepts JSON up to 1MB, forwards as untrusted context, returns 202; enqueues directly through DBOS gates.
    • Auth: per-trigger Better Auth key with { webhook_<triggerId>: ["FIRE"] }, cross-checks org/trigger, and rejects stale tokens by matching api_key_id; 401/403 on invalid/mismatch.
    • Tools/UI: AUTOMATION_TRIGGER_ADD supports type: "webhook" and returns a one‑time URL + token; AUTOMATION_TRIGGER_ROTATE_TOKEN issues a new token and revokes the old; AUTOMATION_TRIGGER_REMOVE revokes the key. Starter adds “Webhook” with a one‑time secret dialog (curl/fetch snippets + “Token in URL”), and Trigger card adds a Rotate action.
  • Bug Fixes

    • Dispatch: on stream errors, write a sanitized assistant message to the thread so failures aren’t silently dropped (critical for cron/webhook/event runs).
    • Sandbox: when sandboxPreference="default" resolves to desktop, bind the user’s link instead of instantiating directly; clearer error if no link is available.
    • Chat UI: collapse Tools button gap and show the tier dropdown label on automation detail via container queries.

Written for commit 54c09a7. Summary will update on new commits. Review in cubic

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Release Options

Suggested: Minor (2.342.0) — based on feat: prefix

React with an emoji to override the release type:

Reaction Type Next Version
👍 Prerelease 2.341.1-alpha.1
🎉 Patch 2.341.1
❤️ Minor 2.342.0
🚀 Major 3.0.0

Current version: 2.341.0

Note: If multiple reactions exist, the smallest bump wins. If no reactions, the suggested bump is used (default: patch).

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 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="apps/mesh/src/tools/automations/trigger-rotate-token.ts">

<violation number="1" location="apps/mesh/src/tools/automations/trigger-rotate-token.ts:76">
P1: Old token can stay valid after rotate. Delete failure is swallowed, and webhook auth does not enforce current `trigger.api_key_id`. Reject non-current key or fail rotation when revoke fails.</violation>
</file>

<file name="apps/mesh/migrations/082-automation-webhook-triggers.ts">

<violation number="1" location="apps/mesh/migrations/082-automation-webhook-triggers.ts:41">
P1: Down migration can fail if webhook triggers exist. The `ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))` will fail when existing rows have `type = 'webhook'`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (oldKeyId) {
try {
await ctx.boundAuth.apiKey.delete(oldKeyId);
} catch (err) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Old token can stay valid after rotate. Delete failure is swallowed, and webhook auth does not enforce current trigger.api_key_id. Reject non-current key or fail rotation when revoke fails.

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/trigger-rotate-token.ts, line 76:

<comment>Old token can stay valid after rotate. Delete failure is swallowed, and webhook auth does not enforce current `trigger.api_key_id`. Reject non-current key or fail rotation when revoke fails.</comment>

<file context>
@@ -0,0 +1,90 @@
+    if (oldKeyId) {
+      try {
+        await ctx.boundAuth.apiKey.delete(oldKeyId);
+      } catch (err) {
+        console.warn(
+          `[trigger-rotate] failed to delete old key ${oldKeyId} for trigger ${trigger.id}:`,
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by enforcing current key id in the webhook auth route. automation-webhooks.ts now rejects with 401 "Stale token" when verified.apiKeyId !== trigger.api_key_id, so even if the best-effort delete in rotate fails (or if any other path leaves a stale key with the FIRE permission), the old token stops working the moment the swap row update commits. Left the rotate delete as best-effort with a log since the auth-side check makes it harmless.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

await sql`ALTER TABLE automation_triggers DROP CONSTRAINT chk_trigger_type`.execute(
db,
);
await sql`ALTER TABLE automation_triggers ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))`.execute(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Down migration can fail if webhook triggers exist. The ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event')) will fail when existing rows have type = 'webhook'.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/migrations/082-automation-webhook-triggers.ts, line 41:

<comment>Down migration can fail if webhook triggers exist. The `ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))` will fail when existing rows have `type = 'webhook'`.</comment>

<file context>
@@ -0,0 +1,44 @@
+  await sql`ALTER TABLE automation_triggers DROP CONSTRAINT chk_trigger_type`.execute(
+    db,
+  );
+  await sql`ALTER TABLE automation_triggers ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))`.execute(
+    db,
+  );
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Followed the convention from migration 078 (chk_automation_kind): delete rows that violate the narrower CHECK before re-applying it in the down migration.

viktormarinho and others added 2 commits May 22, 2026 11:17
Webhook triggers are a third option alongside cron and event, letting external
systems fire an automation via HTTP POST. Authentication uses a Better Auth
API key scoped to the trigger; the token is shown once at creation and can be
rotated. The callback route bypasses the buggy event-bus dispatch path and
enqueues directly through the same DBOS gates as cron/event fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reject stale API keys in webhook auth by comparing against trigger.api_key_id
- Delete webhook rows in down migration before re-applying narrower CHECK
@tlgimenes tlgimenes force-pushed the viktormarinho/webhook-trigger-automation branch from cce28fa to bb6b28e Compare May 22, 2026 14:17
tlgimenes added 5 commits May 22, 2026 11:37
- Add curl/fetch snippet variants and a "Token in URL" checkbox that
  rewrites the URL and removes the Authorization header in lockstep.
- Render the snippet in a readonly Monaco editor for syntax highlighting
  (shell for curl, typescript for fetch).
- Fix dialog content overflowing the modal box (missing min-w-0 on
  flex/grid children with long URLs/tokens).
- Rename "Example" label to "Snippet".

Extends MonacoCodeEditor with a "shell" language option and a
disableDiagnostics prop for clean readonly display.
…-trigger-automation

# Conflicts:
#	apps/mesh/migrations/index.ts
…fail

When `streamText.onError` fires (model API error, credential decrypt
failure, permission denial, etc.), the run was force-finished as
`failed` but no assistant message was persisted. The UI's user/assistant
pairing then drops the orphan and renders "No response was generated".

This is especially painful for background runs (cron, webhook, event-bus
automations) where the user has no console to read the swallowed
`console.error` from. Now the sanitized error is written as a synthetic
assistant message so it shows up directly in the thread and in the DB.
Background automation fires (cron, webhook, event-bus triggers) flow
through dispatch-run with the default `DispatchTarget` (`local`/`default`),
which sets `ctx.sandboxPreference = "default"` but never populates
`ctx.linkForCurrentRun`. In local dev `STUDIO_SANDBOX_RUNNER` is unset,
so `resolveSandboxProviderKindFromEnv()` returns "desktop" — and the
"default" branch was passing that straight to `getSandboxProviderByKind`,
which routes to `instantiate("desktop")` and throws:

  desktop runner cannot be instantiated without a per-run LinkEntry —
  call resolveSandboxProvider, which binds the link before constructing
  the provider.

The error was being swallowed by `streamText.onError`, surfacing in the
UI only as "No response was generated" with 0 tokens.

Route the "default" path through `bindProviderForKind` instead, which
already handles the desktop-needs-a-link case by looking up the user's
link via `ctx.linkRegistry`. Non-desktop env kinds (docker, agent-sandbox)
still fall through to `getSandboxProviderByKind` unchanged.
…automation detail

Spreads the AgentModelTrigger-style collapse fix to two more spots:

- Tools button (chat input) and SimpleModeTierDropdown: add
  `gap-0 @[496px]/chat-bottom:gap-1.5` to the outer button so the
  icon doesn't carry a phantom `gap-2` (Button default) when the
  label is collapsed to max-w-0.
- Automation detail bottom bar: add `@container/chat-bottom` so
  the SimpleModeTierDropdown's `@[496px]/chat-bottom:` container
  queries actually match — the label was permanently hidden there
  before because no parent named the container.
@tlgimenes tlgimenes merged commit 247edcc into main May 22, 2026
17 checks passed
@tlgimenes tlgimenes deleted the viktormarinho/webhook-trigger-automation branch May 22, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants