Automations and webhooks for the AT Protocol airglow.run

Add an "Add to list" action #40

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

Part of https://tangled.org/exosphere.site/airglow/issues/29.

Scope #

A dedicated "Add to list" action that takes a list URI and a subject, and writes an app.bsky.graph.listitem to the owner's repo.

The list URI is still entered by hand here. The picker is a separate issue.

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

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

loop:queued cid=bafyreia3eeedial7x6gho7ldhocj6rycjd7qxgj5mjzkxjxqyd4dhb2zj4

Working on this, attempt 1.

loop:claim run=contribute-20261026-162843-3my34fukr7322 branch=loop/3my34fukr7322 attempt=1

Attempt 1 failed: the session ended: interrupted.

loop:failed run=contribute-20261026-162843-3my34fukr7322 attempt=1 interrupted=1

Working on this, attempt 1.

loop:claim run=contribute-20261026-221459-3my34fukr7322 branch=loop/3my34fukr7322 attempt=1 issue=40

The work is ready but touches owner-only paths: vite.config.ts, so it was not pushed. The diff is in the run directory.

feat(actions): add an "Add to list" action for Bluesky lists

This adds an "Add to list" action (bsky-list-add, lexicon def #bskyListAddAction). It takes a list AT URI and an explicit choice of who to add, and writes an app.bsky.graph.listitem to the owner's repo. It refuses a list owned by another DID with an error that names both DIDs. It also skips an account that is already on the list. Both checks target what went wrong in the Graze session: about 45 minutes of runs that reported "200" while writing to the wrong repo, and the same user added twice.

What changed #

  • lib/actions/bsky-list-add.ts: the new action's validate, execute, dry run and PDS projection, registered in ACTION_REGISTRY. It is wired through schema.ts, steps.ts, pds.ts, validation.ts, the lexicon (the def plus all four step unions) and the test fixtures.
  • Owner check. The rendered list must look like at://<did>/app.bsky.graph.list/<rkey>, and <did> must be the automation's DID. The check runs in three places:
    • At save time, for a literal list. validateStepTree now takes an optional ownerDid, which both automation routes pass, and it reaches validators as ValidationContext.ownerDid.
    • At run time, as a 400 that is not retried.
    • In dry run, which reports the same error, so it shows up while testing.
  • Duplicate check. Before writing, a search over the owner's listitems looks for a matching list and subject. A hit logs a 204 skip ("already on the list"). Writes for the same owner, list and subject also run one at a time in the process, so two events that arrive together can't both pass the check. That is how the session's duplicate happened: two listitems 1 s apart.
  • Who to add. subjectFrom is "author" (adds event.did) or "record" (adds the DID template in subject, e.g. {{event.commit.record.subject}}). In the editor this is a required select with no default, so the author has to pick one. Picking "A DID from the record" fills in the trigger lexicon's first did-format field (new TriggerExamples.did). The detail page shows which choice was made.
  • UI: a new bsky-list-add.tsx editor and display block, with an "Add to list" tile in Apps right after "Post to Bluesky" (Bluesky colour, list-plus icon).
  • list joins ADDRESSING_FIELDS, so an AI answer can't choose which list is written to (the same rule as subject and uri).

Verification #

  • pnpm run check: passes.
  • pnpm run test: 173 files, 2684 tests pass.
  • New tests:
    • lib/actions/bsky-list-add.test.ts:
      • the write's shape, and both subject choices
      • refusing a foreign list and a non-list URI, with no write and no retry
      • skipping a subject already on the list
      • two simultaneous events producing one write; temporarily removing the per-key lock makes this test fail
      • fail-open when the search errors, and retry on 5xx
      • validation (save-time owner check, explicit choice required), toPds and dry run
    • validate-steps.test.ts: ownerDid reaches the validator, and list counts as an addressing field.
    • bsky-list-add.test.tsx: the select starts empty and is required; the subject field shows only for "record" and is pre-filled.
  • I did not try it in a browser. This container has no dev server or PDS.

Acceptance criteria #

Decisions you can reverse #

  • Naming. The step type is bsky-list-add / #bskyListAddAction, and the choice field is subjectFrom: "author" | "record". When the choice is "author", no subject is stored.
  • No default for who to add. The select starts empty. The right answer depends on whose repo the trigger record lives in: Sifa needs "record", You and Me needs "author".
  • The duplicate check fails open. If the search errors, the write still goes ahead, which matches the follow action. The per-key lock covers a single process only.
  • Strict list URI format. Only at://did:.../app.bsky.graph.list/<rkey> is accepted. A handle authority or a bsky.app URL is refused with an error that shows the expected format. Placeholders such as {{self}} are fine.
  • No list-exists check. A list that doesn't exist (but is in the owner's repo) is not detected before writing.
  • Where the tile sits. It is in Apps after "Post to Bluesky", because CatalogueTile.category has no "social" option.

loop:awaiting-owner run=contribute-20261026-221459-3my34fukr7322 why=owner-only

done

Sign up or Sign in to add to the discussion.