SuperAPI AI Directory

subscribing-relay-webhooks-and-writebacks

Agents & models

View repository

Updated 10-15 20:11

About this skill

Subscribe to Webhooks and Write Back to Providers

Two directions, one session:

  • Inbound (subscribe): provider events arrive in this session as injected messages from @__relay_webhook__.
  • Outbound (write back): the agent writes a file into a Relayfile mount and the cloud delivers it to the provider.

Both need a signed-in, registered desktop. If GET /setup/status does not show sign_in: signed_in and session.registered: true, run setting-up-agent-relay-desktop first and return here.

Everything below was verified on macOS with Agent Relay Desktop 2026.10.6 and relayfile 0.10.71. Known defects are linked to their issues in AgentWorkforce/relayfile-cloud; do not paper over them, report them.

Safety first

  • A write-back posts a real message under the workspace's bot. Confirm the channel or repo with the human before the first write and say it will be visible. Label test messages as tests. Never pick #general or an investor/hiring channel for a test.
  • Identical content posted twice dedupes to one Slack message; use unique content (a timestamp) when a fresh visible post is needed.
  • Never write under <mount>/.relay/; it is daemon state.
  • Never put a webhook secret, bearer token, or workspace key in a Relay message, argv, or the repo.

Part 1: inbound

Set up the request helper and confirm the session

Define these once per shell; every request in this skill goes through relay_req, which uses the Desktop's agent-relay-probe on macOS (the system curl hides a Codex session's identity there and is refused not_a_relay_session), curl on Linux and for Claude Code, and stops with a message for a macOS Codex session that has no probe:

relay_socket=$(sed -n '1p' "$HOME/.agentworkforce/desktop/relay-socket")
relay_probe=
for c in "$HOME/.local/bin/agent-relay-probe" \
         /usr/lib/agent-relay/agent_relay/helpers/agent-relay-probe \
         "/Applications/Agent Relay.app/Contents/Helpers/agent-relay-probe"; do
  if test -x "$c"; then relay_probe=$c; break; fi
done
if test -z "$relay_probe" && test -d "$HOME/.local/lib/agent-relay"; then   # per-user tarball
  relay_probe=$(find "$HOME/.local/lib/agent-relay" -type f -perm -u+x \
    -path '*/agent_relay/helpers/agent-relay-probe' -print -quit 2>/dev/null)
fi
# relay_req METHOD PATH [JSON-BODY]
relay_req() {
  if test "$(uname -s)" = Darwin && test -n "$relay_probe"; then
    printf '%s' "${3:-}" | "$relay_probe" relay socket-request \
      --socket "$relay_socket" --method "$1" --path "$2"
  elif test "$(uname -s)" = Darwin && test -n "${CODEX_THREAD_ID:-}"; then
    echo 'A Codex session on macOS needs agent-relay-probe; start Agent Relay and retry.' >&2
    return 1
  elif test -n "${3:-}"; then
    curl -sS --unix-socket "$relay_socket" -X "$1" \
      -H 'Content-Type: application/json' -d "$3" "http://relay$2"
  else
    curl -sS --unix-socket "$relay_socket" -X "$1" "http://relay$2"
  fi
}
integration() {   # integration subscribe|unsubscribe PROVIDER RESOURCE
  local m=
  case "$1" in
    subscribe) m=POST ;;
    unsubscribe) m=DELETE ;;
    *) echo "integration: expected subscribe|unsubscribe, got '$1'" >&2; return 2 ;;
  esac
  relay_req "$m" /integrations/subscribe \
    "$(jq -nc --arg p "$2" --arg r "$3" '{provider:$p,resource:$r}')" |
    jq -c '{ok, subscribed: .data.subscribed, resource: .data.resource, code: .error.code}'
}

Then confirm the session is signed in and registered:

relay_req GET /setup/status |
  jq '.data | {sign_in, address: .session.address, registered: .session.registered, direct_delivery}'

Require sign_in: "signed_in" and registered: true. Run it from this agent's own shell inside an interactive session; another terminal or codex exec is refused with not_a_relay_session.

Reinstalled or re-registered? Wiping local state does not release the cloud registration. POST /register can answer name_taken; register under a new name with {"name":"<new-name>"} and tell people the new address, because messages to the old address go to an offline agent and are never injected.

Subscribe to a GitHub pull request

A PR needs four globs: its pull data, reviews, checks, and GitHub's separate issue-comment path.

R=OWNER/REPO; N=NUMBER
for g in "/github/repos/$R/pulls/$N/**" \
         "/github/repos/$R/pulls/$N/reviews/**" \
         "/github/repos/$R/pulls/$N/status/**" \
         "/github/repos/$R/issues/$N/comments/**"; do integration subscribe github "$g"; done
relay_req GET /setup/status | jq -c '.data.integrations'

Require ok: true and subscribed: true for each, and every entry ready: true in the status list. A repeat answers HTTP 409, shown as code: conflict ("already subscribed"); treat that one code as success.

Make this a required step right after the agent opens a PR, so review feedback arrives without anyone naming the PR. A subscribe response alone does not prove delivery.

Upgrade note. A one-command form, agent-relay-probe relay subscribe <PR URL>, is in AgentWorkforce/relay-desktop#216 and is not in the latest release (2026.10.6 reports unrecognized subcommand 'subscribe'). Use the four globs until a release ships it, then re-check with agent-relay-probe relay --help.

Subscribe to a Slack channel

This lookup uses relayfile, so first run Part 2's "Install and authenticate relayfile" (npm install -g relayfile, then relayfile login </dev/null); without it the command fails with command not found or delegated relayfile credentials are required. Then find the channel id and subscribe to both directory spellings, because a channel can appear as <id> or <id>__<slug> (the integration helper is defined at the top of Part 1):

relayfile read /discovery/slack/channels/_index.json |
  jq -r '.. | objects | select(.name? == "CHANNEL-NAME") | .id'
integration subscribe slack "/slack/channels/CHANNEL_ID/**"
integration subscribe slack "/slack/channels/CHANNEL_ID__CHANNEL-NAME/**"

Verify delivery

Relay injects an event only when the session is idle between turns. After causing a harmless authorized event (a test comment, a requested bot review, a labelled test post), end the turn and confirm on the next turn that the injected message arrived with its full message id. Do not sleep or poll.

Expect noise: PR bots echo your own replies back as review_comment.created, empty "COMMENTED" review wrappers, and synchronize for your own pushes. Check gh api before acting; an echo of your own reply needs no response.

Webhook creation and test

Creating a webhook is a persistent external change; do it only when asked. Follow setting-up-agent-relay-desktop section 5 (one-time secret to a mode-0600 temp file, unique marker, delete the file, confirm the marker next turn).

Undo

Remove a subscription with the same helper, once per glob:

integration unsubscribe github "/github/repos/$R/pulls/$N/**"   # repeat for each glob

Do not DELETE /webhooks as part of undoing subscriptions. That removes the workspace's shared webhook, which other subscriptions and agents may rely on, and its secret cannot be recovered. Do it only with the human's explicit authorization, and only for a webhook this procedure created.

Part 2: outbound write-backs

Install and authenticate relayfile

npm install -g relayfile
relayfile --version            # 0.10.x
relayfile login </dev/null     # reuses the desktop's agent-relay session
relayfile status               # expect: auth: agent-relay session ok

Do not use relayfile login --no-open: it is listed in --help but fails with unknown option '--no-open' (relayfile-cloud#282). If a mount reports delegated relayfile credentials are required, run relayfile login once; it creates ~/.relayfile/delegated/*.

A provider that is not yet connected to the workspace is connected with relayfile integration connect <provider> --no-open, which prints a Nango connect URL for the human to approve in a browser (the URL expires in about 30 minutes). Check what is already connected with relayfile integration list. Do not disconnect an integration to "test" first-run: it breaks live users.

Mount only what you need

Mount one subtree, not the workspace. A channel can live under <id> or <id>__<slug>; resolve the directory that actually exists and mount that exact path, because --local-layout exact puts only the selected subtree at the local root:

chan_dir=$(relayfile read /discovery/slack/channels/_index.json |
  jq -r '.. | objects | select(.id? == "CHANNEL_ID") | .path' | head -1)
printf 'mounting %s\n' "$chan_dir"      # must be non-empty and the one you subscribed to
test -n "$chan_dir" || { echo 'channel not found in the discovery index' >&2; exit 1; }
mount_dir=~/relayfile-mount-test; mkdir -p "$mount_dir"
nohup relayfile mount --local-dir "$mount_dir" --local-layout exact \
  --remote-path "$chan_dir" >/tmp/relayfile-mount.log 2>&1 &
echo $! > /tmp/relayfile-mount.pid       # kept for cleanup; do not pkill by name

The index row's path is the directory the workspace actually uses for that channel (/slack/channels/<id> or /slack/channels/<id>__<slug>). Do not take it from relayfile tree /slack/channels: that listing is paged and a channel may not be on the first page.

Use a single --remote-path. Multiple paths are currently impossible: exact is rejected for them and scoped is disabled (#280). With exact, the local root is the mounted subtree itself (messages/, meta.json). The mirror syncs about every 30 seconds.

Discover the write contract; do not guess

relayfile read /discovery/slack/.adapter.md
relayfile read '/discovery/slack/channels/{channelId}/messages/.create.example.json'

The docs live under /discovery/<provider>/. The paths printed inside .adapter.md omit that prefix, and {channelId} is a literal directory name (#283). For Slack, create = write JSON with any non-canonical filename into <mount>/messages/; at least one of text, blocks, attachments is required; add an idempotencyKey so a retry cannot double-post.

Post a draft and verify

Do not write until the mount is up and its messages/ directory exists; a draft written too early fails with a missing path instead of producing a receipt:

mount_dir=${mount_dir:-$HOME/relayfile-mount-test}
mount_alive() { kill -0 "$(cat /tmp/relayfile-mount.pid 2>/dev/null)" 2>/dev/null; }
for i in $(seq 1 45); do                      # up to ~90s for the first sync
  mount_alive || { echo 'mount is not running; see /tmp/relayfile-mount.log' >&2; exit 1; }
  test -d "$mount_dir/messages" && break
  sleep 2
done
# A leftover messages/ from an earlier mirror does not count: the mount must be alive.
mount_alive || { echo 'mount is not running; refusing to write a draft' >&2; exit 1; }
test -d "$mount_dir/messages" || { echo 'messages/ never appeared' >&2; exit 1; }

ts=$(date -u +%Y%m%dT%H%M%SZ)
draft="$mount_dir/messages/wb-test-draft-$ts.json"
jq -nc --arg t "[writeback test $ts] <what this verifies>. Safe to ignore." \
       --arg k "wb-test-$ts" '{text:$t,idempotencyKey:$k}' > "$draft"
for i in $(seq 1 30); do                      # poll up to ~90s for the receipt
  grep -q '"created"' "$draft" 2>/dev/null && break
  sleep 3
done
if ! grep -q '"created"' "$draft" 2>/dev/null; then
  echo 'no receipt after ~90s: the draft was NOT delivered (still a plain draft)' >&2
  relayfile writeback list --state pending
  relayfile writeback list --state dead
  exit 1
fi
cat "$draft"                                  # the receipt
relayfile writeback status       # want pending: 0  failed: 0  dead-lettered: 0

Success is the draft being rewritten as a receipt {"created":..., "path":..., "externalId":"<ts>", "ts":"<ts>"} and dead-lettered: 0. pending should drain to 0 within about 30 seconds. If it stays pending, look at relayfile writeback list --state pending and the mount log first. Only a dead-lettered op is retried, with its workspace: relayfile writeback list --state dead, then relayfile writeback retry --op-id <op> <workspace-id> (the CLI also accepts --opId).

Do not expect the canonical record to appear. After a successful post, <mount>/messages/<ts>/meta.json may never exist, and reading it returns 404 (#277). The receipt's ts is the proof Slack accepted the message. Slack's provider status may also read lagging indefinitely (#278); that alone is not an outage.

Other write shapes

Edit a message: write mutable fields to the canonical <ts>/meta.json (only once it exists). Reply or react: use the sibling replies/ and reactions/ resource directories from .adapter.md. Delete: remove the canonical file only when .adapter.md says delete is supported.

Failure cheat sheet

Symptom Meaning Do
not_a_relay_session Not an interactive Claude/Codex session Run from the agent's own shell
name_taken on /register Old cloud registration survives a reinstall Register a new name; tell people the new address
Messages to the old address never arrive Old agent is offline Use the new address; agent-relay agent remove <old> after confirming
429 workspace_busy on relayfile read Shared workspace admission pool is full; the CLI does not retry Wait the advertised delay and retry; stop heavy mounts (#279)
relayfile ops list warns credentials.json not found Reads the legacy credential store Ignore the warning; use writeback status (#281)
delegated relayfile credentials are required No login on this machine relayfile login </dev/null
.schema.json 404 under the resource Docs are under /discovery/... Read from /discovery/<provider>/...
Draft never becomes a receipt Mount not running or write-back failing relayfile status, mount log, writeback list --state dead

Clean up

Stop only the mount this procedure started, using the PID recorded at start, and confirm it is still that process first. Never pkill -f 'relayfile mount': it matches every mount on the machine, including other people's live mirrors and pending write-backs.

pid=$(cat /tmp/relayfile-mount.pid 2>/dev/null)
if test -n "$pid" && ps -o command= -p "$pid" | grep -q 'relayfile mount.*relayfile-mount-test'; then
  kill "$pid" && rm /tmp/relayfile-mount.pid
fi

Remove the throwaway local mirror, delete subscriptions you no longer need, and say what you left running.