Skip to content

feat(friend): workflow triggers as their own nodes, with webhook test mode and a bot trigger - #3

Merged
IsraelAraujo70 merged 43 commits into
feat/workflows-integrationfrom
feat/workflows-triggers
Nov 2, 2026
Merged

IsraelAraujo70 merged 43 commits into
feat/workflows-integrationfrom
feat/workflows-triggers

Conversation

@IsraelAraujo70

Copy link
Copy Markdown
Owner

Stacked on #2. Review that one first; this diff is only the triggers work on top of it.

Why

The owner asked for triggers that read as their own things, the way n8n does it: webhook, manual and bot triggers you can see, switch and test. In #2, triggers live only in the YAML. Manual (Run now) and bot (workflow_run) runs are implicit, and the canvas shows no trigger at all.

What changed

  • Schema. triggers becomes a map keyed by id, sharing the node id namespace. Three kinds are now declared: manual, bot (a description and an optional bots allow-list) and call (an optional from list). A workflow has at most one of each. The six event kinds stay, each with an optional title, and the four library templates migrate. Migration 014 is edited in place, since it never ran in production.
  • Per-trigger switch. A TriggerToggled fact, on by default, works next to the workflow's switch. One pure function, triggerBlock in triggers/policy.ts, decides whether a fired trigger becomes a run. The workflow decider is the authority; the RPC and MCP handlers call the same function to refuse early.
  • Manual. Run now appears only when the workflow declares a manual trigger. Runs the owner starts from their own session ignore both switches, like n8n's "Execute workflow"; suspension still blocks them.
  • Webhook. The request arrives structured as trigger.body, trigger.headers (redacted) and trigger.query, capped at 256 KiB, through the existing ScheduledTaskService seam.
    • The last event per trigger is stored in workflow_trigger_samples.
    • "Listen for test event" holds the real URL for 120 s. A captured delivery goes through the same signature check, filters and dedupe as a real one, but starts no run.
    • "Run with this event" starts a run from the captured sample, so the params mapping and the {{ trigger.body.* }} expressions run for real.
    • A capture writes a per-delivery marker in the same transaction, so a redelivery after a crash is acknowledged without a run.
  • Bot. The bot trigger is the only way a bot can run a workflow. workflow_list shows bots the trigger's description and parameter schema, and whether a run would start now. The rule from feat(friend): workflows on the bots host, with hidden step threads #2 still applies on top: a bot without canManageBots may run or enable a workflow only during a turn that came from the owner's own message. Gates stay owner-only.
  • Call. A child workflow can restrict which parents may call it.
  • Canvas. Each trigger is an entry node on the left, with its own icon, color and shape, and dashed edges to the graph's roots. Its inspector depends on the kind: webhook URL with copy, last event and test mode; bot allow-list. On the run page, the trigger that fired is highlighted, and its payload loads only when its card opens, never in the run snapshot or deltas.
  • Android. Triggers appear as chips above the graph, with a switch per trigger and the webhook URL. The run screen loads up to 4 KiB of payload.
  • Prompts. trigger.* values inside an agent prompt are wrapped as untrusted data.
  • Docs and evals. FORK.md, the user guide and the design doc are updated. Three mcp-tool-use cases are added.

Scope

  • No new dependencies.
  • Upstream edits: only the existing ScheduledTaskService.ts seam, which now passes the request through; its scripts/friend/seams.ts entry is updated.
  • Out of scope: a step detail panel already printed null for a command without parse before this PR. It is still unfixed.

Blast Radius

Workflows and their MCP tools only. Changes in behavior:

  • A workflow without a manual trigger shows no Run now button.
  • A bot can run only workflows that declare a bot trigger listing it, or open to any bot.
  • A test capture uses up the delivery's replayKey, so a redelivery with the same key is dropped later; the user guide says so.

The deploy is the same as #2: migration 014 at boot, vp i, a web rebuild, a server restart, and new builds of the Mac app and the APK.

Verification

  • vp run check:changed friend/main passes: format, lint, knip, typecheck of 39 packages, 89 test files and the React Compiler check. node scripts/friend/check-seams.ts: OK. 207 test files with 2,417 tests pass across the touched suites.
  • Real webhook against a dev server of this branch: an RPC driver passes 21 of 21 checks (~/.cache/friend-tmp/triggers/proof/out/proof.log on the bots host).
    • Test mode captures without a run.
    • "Run with this event" carries issue 42 from {{ trigger.body.issue.number }} into the step.
    • Deliveries are skipped as disabled when the workflow or that trigger is off.
    • A filter drops events that don't match.
    • Run now goes through the manual trigger.
    • run.trigger returns the payload and truncates it above maxBytes.
  • Tests:
    • decider and policy tests for each trigger kind;
    • handler tests for the bot trigger combined with the owner-turn rule;
    • a test for the redelivery-after-capture case;
    • a browser test that keeps Save live while the trigger inspector has focus.
  • Screenshots in light and dark through the T3 browser: the list with trigger chips, the canvas with four trigger nodes, the webhook inspector, a captured event, the run page with the fired trigger and its payload, a switched-off trigger, and Run now. They are indexed in ~/.cache/friend-tmp/triggers/proof/shots/INDEX.md on the bots host.
  • Interrogate: a Sol @high loop approved in round 2. Round 1's three findings were fixed:
    • a test listener could outlive Stop and swallow a real delivery;
    • Listen ignored the permission and showed no error;
    • a trigger with id call captured child runs.

Not verified here:

  • Android on a device: no device or emulator was available. The trigger chips pass typecheck, tests and the React Compiler check.
  • The paid eval needs an API key, and the eval runner does not load the fork's toolkits yet: cd evals && pnpm mcp-tool-use --case workflow-run-needs-bot-trigger,workflow-run-uses-bot-params,workflow-enable-one-trigger --out runs/workflow-triggers.

Work by Claude Opus 5.5 (lead) with GPT-6.1 Sol workers, in T3 Code.

🤖 Generated with Claude Code

IsraelAraujo70 and others added 30 commits November 2, 2026 17:00
Triggers become a map keyed by id with three declared kinds (manual,
bot, call), a switch per trigger (TriggerToggled), the trigger id on run
summaries, the webhook sample and test-mode RPCs, and the run trigger
payload read on demand. The policy, samples and shared helpers land
with their first consumers; the server and clients compile again next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
The compiler, trigger sources, catalog, definitions summaries and the
four library workflows read `triggers` as a map. Run summaries and run
views carry the trigger id; the trigger-sample and test-mode RPCs answer
"unavailable" until their services land.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…liveries

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Moves the bot and parent caller lookup out of the decider into
`callerOf`, reads trigger ids with `Object.hasOwn`, and keeps imports
below the module docblock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`botEntries` takes the definition, switches and trigger states from each
workflow's document instead of recompiling HEAD and reading the status
twice; trigger ids are looked up with `Object.hasOwn`. The test now
covers the allow-list and the workflow switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
The style registry now has `trigger:<kind>` rows; the palette lists node
kinds only, so it skips them instead of offering buttons that would add
nodes of a kind that does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
A listener that registers while a test event is being stored stays in
the set and waits for the next delivery instead of being dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
…eplay an event

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
IsraelAraujo70 and others added 13 commits November 2, 2026 18:23
…es over MCP

Covers the "no bot trigger" and "not allowed" refusals for a manager
bot, the `bot` entry `workflow_list` returns, and `triggerId` reaching
`setEnabled`. A param error now reads `count: expected number` once,
not with the name twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trigger nodes get a source handle, an "Off" pill when switched off, and
on a run the static ok halo for the trigger that started it (the others
dimmed). Entry edges draw dashed, carry no label and cannot be edited.
Clicking a trigger selects it. Client atoms read a run's trigger payload,
a trigger's last event and the webhook listen stream.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
A node or trigger named like an Object property (`constructor`) no
longer opens the trigger inspector or reads as taken. "Run with this
event" shows the server's reason when the run cannot start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nual trigger

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
The guide explains Run now without arming the workflow, trigger nodes
and their own switches, the webhook test mode (and that a test delivery
uses up its replay key), and the bot trigger. The design doc's §5 now
describes the trigger map, the policy, the bot trigger, samples and the
test mode, and its YAML excerpts use the map form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
The listen subscription kept the shared five-minute idle TTL, so after
"Stop" the server kept a listener and could capture the next real
delivery without a run; it also had no terminal predicate. It now
releases at once and completes on Captured or Expired. The panel disables
Listen and "Run with this event" without the matching permission and
shows a listen error. A child run's "call" placeholder no longer resolves
to a trigger of another kind that happens to be named `call`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contracts test lists the read-scoped workflow RPCs; it now includes
`run.trigger` and `trigger.sample`. The Android trigger switch drops a
try/finally the React Compiler cannot lower, as the workflow switch does
(the command resolves to a result, it does not throw).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Nov 2, 2026
@IsraelAraujo70
IsraelAraujo70 merged commit 0fcb7dc into feat/workflows-integration Nov 2, 2026
7 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant