A runnable Vercel AI SDK agent that creates, updates, and deletes Linear labels through Relayfile — no Linear API client, no OAuth, no provider glue. The agent reads the labels schema, builds a schema-valid payload, writes a draft, polls for the adapter to materialise the canonical record, then can read/update/delete it.
| Bootstrap | Cloud token → joinWorkspace → client(), identical template to the Notion read example |
| ID handling | App-UUID in, rw_ ID out — every data-plane call uses workspace.workspaceId |
| Least-privilege | Requests relayfile:fs:read:/discovery/linear/**, relayfile:fs:read:/linear/**, relayfile:fs:write:/linear/** (no wildcard write) |
| Schema discipline | Reads /discovery/linear/labels/.schema.json and validates the payload before writing — the playbook's "schema-validated > LAYOUT-documented > skip" rule |
| Draft-create flow | Writes to a non-canonical filename (/linear/labels/relayfile-writeback-test--<ts>.json), polls for the adapter to rewrite it as { created, path, url }, reads the canonical record |
| Optimistic concurrency | Demonstrates RevisionConflictError by writing with a stale baseRevision |
| Self-cleaning | Deletes the created label at the end of the smoke so the workspace stays tidy |
Every smoke run creates ONE real Linear label in the connected workspace and
deletes it before exit. All artifacts are clearly marked
relayfile-writeback-test <ISO-utc> so an operator can identify and remove
any orphan if the smoke is interrupted mid-flight. Look under
/linear/labels/relayfile-writeback-test-* if you need to clean up.
The smoke performs two delete calls; their provider-side semantics differ:
| Delete target | Path shape | Linear-side effect |
|---|---|---|
| Canonical | /linear/labels/<uuid>.json (UUID-matching) |
Propagates — real Linear label is deleted via GraphQL. |
| Draft receipt | /linear/labels/relayfile-writeback-test--*.json (non-UUID) |
Local-only — Relayfile-side cleanup, does NOT enqueue a Linear writeback. Per cloud path-eligibility.ts, the Linear delete writeback only fires for paths matching /linear/(issues|labels|projects)/<uuid>.json. |
That means deleting the draft receipt is a free no-op writeback-wise; no risk of accidentally double-firing a Linear delete.
The Linear adapter writes a receipt to the draft path with
{ created, path, externalId } after the canonical is materialized in
parallel at /linear/labels/<externalId>.json. The discovery doc at
/discovery/linear/.adapter.md claims drafts are renamed to the canonical
path, but the runtime currently writes the receipt in place instead — tracked
as AgentWorkforce/relayfile-adapters#213.
The example follows the real contract: read externalId from
providerResult, operate on /linear/labels/<externalId>.json, then clean
up the draft receipt explicitly. You can also use
client.sweepWritebackDrafts({ workspaceId, pathPrefix: "/linear/labels/", apply: true })
to clear accumulated drafts in bulk.
Same credential resolution as the Notion read example — three options:
One-time setup:
agent-relay cloud login # writes ~/.agentworkforce/relay/cloud-auth.json
export CLOUD_WORKSPACE_ID=<your-app-uuid>
npm installFor CI: set CLOUD_API_ACCESS_TOKEN (and optionally CLOUD_API_REFRESH_TOKEN,
CLOUD_API_URL) instead of running login.
npm run smokeRuns the full create → read → update → conflict → delete → verify loop.
export ANTHROPIC_API_KEY=sk-ant-…
npm run devThe LLM picks the tools to call. It's instructed to always prefix label names with the test marker and to delete after itself.
── create label via draft + adapter rewrite ──
result : ✅ PASS
evidence : {
"draftPath": "/linear/labels/relayfile-writeback-test--1718711000000-ab12.json",
"created": "abc12345-…",
"canonicalPath": "/linear/labels/abc12345-….json",
"url": "https://linear.app/…"
}
…
── optimistic update with stale baseRevision → RevisionConflictError ──
result : ✅ PASS
evidence : { "expectedRevision": "rev_X", "currentRevision": "rev_Y", "conflictDetected": true }
── verify deletion: canonical now 404s ──
result : ✅ PASS
evidence : { "confirmedDeleted": true, "status": 404 }
Labels are the highest-leverage Phase-1 writeback target:
- Schema is non-empty with
nameas the only required field — noteamIdlookup needed for workspace-level labels. - Reversible — deleting a label is a no-op for real users vs. closing an issue that someone might be working on.
- Idempotent test-marker in the name means orphans are trivially identifiable for cleanup.
Issues and projects have richer write paths and are demoed in dedicated future examples once labels are proven.
Every SDK call here is pinned against docs/integrations/SDK-SURFACE.md.
Linear write-field contracts are in /discovery/linear/.adapter.md on the
data plane.
Honest answer in packages/agents/README.md: the MCP wins for one-shot single-provider work; @relayfile/agents is structurally better as integrations and agents compound.