Automations and webhooks for the AT Protocol airglow.run

Placeholder list offers event.commit.record.repo, which never resolves #7

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

Problem #

app/routes/api/lexicons/[nsid].ts:41 prepends a synthetic { path: "repo" } field to every lexicon. AutomationForm.tsx:1103-1106 turns every field into event.commit.record.${path}, so the placeholders panel offers event.commit.record.repo.

lib/jetstream/matcher.ts:57 only maps the bare legacy "repo" to event.did. The full path walks commit.record.repo, gets undefined, and any condition using it is never true. validate-steps.ts accepts it because the root is event, so there is no error.

It also has the same description as event.did ("DID of the repo that emitted the event"), so users can't tell them apart.

Seen in the session #

In https://www.youtube.com/watch?v=5MnT-ZgbBIc:

  • 0:39:50 and 0:47:30: "why is this different than this? Those say the same thing to me."
  • About 1:13: switching the condition to it makes the automation stop firing, with nothing in the logs.

Fix direction #

  • Stop advertising repo as a record field (expose event.did only), or map event.commit.record.repo to event.did in the matcher.
  • Have validation flag record paths that aren't in the lexicon schema.

Details: docs/brainstorms/2026-10-17-graze-conference-automation-feedback.md (B3).

What’s still open from the issue:

  • Save-time validation is still missing. The plan deferred it on purpose, because the server has no resolved schema at save time and lexicons can allow extra fields. Without it, API callers get no signal.
  • Saved automations aren’t changed. Ones that already use event.commit.record.repo won’t start matching; they show the warning when edited.

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreibmnr7hxmtrfhlfa5vulncvl35jlazxarrtb7xq7el4mq73yyw7iu

Working on this, attempt 1.

loop:claim run=contribute-20261024-143451-3my32exukza22 branch=loop/3my32exukza22 attempt=1 issue=7

feat(automations): warn on undeclared trigger record paths when saving

Saving an automation now also returns warnings for any event.commit.record.* path that the trigger lexicon does not declare, such as event.commit.record.repo on app.bsky.graph.follow. Until now only the form showed this warning (a0b7a89), so API callers had no way to know about it (owner comment on #7). The warnings don't block the save, which keeps the plan's rule against false rejections: records can carry fields their lexicon doesn't declare, and the server can't always resolve a schema.

What changed #

  • POST /api/automations and PATCH /api/automations/:rkey (when steps is sent) add warnings: [{ stepId, path, message }] to the response when something is flagged. Responses without warnings look the same as before.
  • Paths that are checked:
    • condition check fields
    • loop paths
    • every {{event.commit.record.*}} placeholder in any step: action templates, condition values, variable values and the like. Comments and notes are skipped.
  • For repo, the message points to event.did.
  • Nothing is flagged under unions, unknown or arrays, or when the lexicon can't be resolved.
  • lib/lexicons/load-record-schema.ts loads the schema the same way /api/lexicons/:nsid does: from the cache first, then a live resolve that gets cached. It's only called when the tree actually mentions event.commit.record..
  • recordPathStatus moved from app/islands/step-drafts/field-types.ts to lib/lexicons/record-path.ts, so the form and the server use the same function. field-types.ts re-exports it.

Verification #

  • pnpm run check: passes.
  • pnpm run test: passes (162 files, 2552 tests).
  • New unit tests in lib/automations/record-path-warnings.test.ts: the repo hint, declared paths, $type, union/array/unknown nodes, placeholders inside nested branches, loop paths, comments skipped, duplicates collapsed, no schema, and the lookup skipped when nothing reads the record.
  • New route tests: POST returns 201 with a warning for event.commit.record.repo and no warnings key for a declared path. PATCH returns 200 with a warning naming the step id.
  • I did not test this in a browser. The form doesn't read these warnings, and its own inline warning is unchanged.

Issue checklist #

Decisions you can reverse #

  • Warnings, not a 400. Rejecting the save would be a one-line change in the routes. The catch is that it would also block lexicons whose records carry extension fields, and any edit to an automation saved before this fix.
  • Placeholders in templates are checked too, not just condition fields. The plan had deferred this, but a placeholder on the phantom field renders empty without any sign, which is the same failure.
  • warnings is left out when empty, rather than always sent as [].
  • A cold save can be slower: the first save for a lexicon that isn't cached may wait on resolving it (a 10s fetch timeout per attempt). The form usually fills the cache already.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3my32exukza22

loop:pushed run=contribute-20261024-143451-3my32exukza22 sha=2505be6db74c04ce54cd14270e5be0ad1ab91495

Verdict: merge. The branch handles the last open item of issue #7. When an automation is saved, the server now flags event.commit.record.* paths that the trigger lexicon does not declare, and event.commit.record.repo gets a hint to use event.did. The warnings do not block the save, which keeps the owner's point that lexicons may allow extra fields. pnpm run check and pnpm run test both pass (162 files, 2552 tests).

What I checked and how:

  • Issue scope. The form already stopped offering repo (a0b7a89) and the migration script for saved automations exists (12afb12). Both commits are in this branch's history. This branch adds the missing save-time signal for API callers, as the owner's comment asks. It does not change runtime or matcher behaviour, and saved automations are left alone.
  • Changed code, line by line:
    • recordPathStatus moved unchanged to lib/lexicons/record-path.ts, which has only type imports, so it stays safe for the client bundle. field-types.ts re-exports it.
    • loadRecordSchema matches /api/lexicons/:nsid: cache, then resolve, then setCache. It never throws.
    • recordPathWarnings checks condition fields, loop paths and {{...}} placeholders in every step at any depth. It skips comments and variable notes, skips secret: refs, collapses duplicates per step, and only looks up the schema when the tree mentions event.commit.record..
    • POST checks after its existing NSID allow/deny check. PATCH only checks when steps was sent and uses the validated tree.
    • The warnings key is left out when empty, so existing clients are unaffected.
  • Tests. The new unit tests cover the repo hint, $type, union, array and unknown nodes, nested branches, loop paths, comments, duplicates and a missing schema. The route tests cover POST warning vs. no warning and PATCH with a step id.

Minor note, not required for this merge: the server reads the record path with [\w$]+ segments, while the form splits on .. A declared property whose name has a hyphen could therefore get a false warning from the server but not from the form. This is rare in lexicons, and the warning doesn't block the save.

loop:verdict run=review-20261024-144354-3my32exukza22 sha=2505be6db74c04ce54cd14270e5be0ad1ab91495 verdict=merge

Merged main into the branch: 3909dd5988e85944680d6bcd929a6e38381dd5e6.

loop:carry from=2505be6db74c04ce54cd14270e5be0ad1ab91495 sha=3909dd5988e85944680d6bcd929a6e38381dd5e6

Merged onto main as 6df6e317ccbc740ba96ba9fdd98b1bc372c4f96b.

loop:merged sha=6df6e317ccbc740ba96ba9fdd98b1bc372c4f96b head=3909dd5988e85944680d6bcd929a6e38381dd5e6

Sign up or Sign in to add to the discussion.