The failure mode
A timeout is not a rollback
HTTP clients tend to reduce a request to “success” or “failure.” A side-effecting API has a third state: the server committed the write, but the client never received the response. Retrying a comment, transition, or label mutation can repeat the effect. Refusing to retry can lose the intended update.
async function addComment(issueId: string, body: string) {
// The provider may commit this request before the connection times out.
await provider.comments.create({ issueId, body });
}
// A queue retries after the timeout. Now the issue may have two comments.
await retry(() => addComment("ENG-123", summary));Agents amplify this problem. They run in queues, workflows, and tool loops where retries are normal. They may also act on an issue snapshot that a human changed seconds earlier. Duplicate comments are visible; a stale overwrite is quieter and often worse.
“Have I executed this intent before?” is idempotency. “Is the item still the version I inspected?” is optimistic concurrency. You need both.
API design
Put a transaction boundary before the provider call
The useful abstraction is not another method named updateIssue. It is a reviewable value that separates intent from execution. Preparing the change reads current state, resolves provider semantics, calculates a field-level diff, and records the expected revision. Committing verifies that plan before making the irreversible request.
const change = await work.prepareComment("ENG-123", {
body: summary,
});
// A policy, agent, or person can inspect the exact proposed side effect.
console.log(change.summary, change.warnings, change.expectedRevision);
const receipt = await work.commit(change, {
// Stable business-event key, not a random attempt ID.
idempotencyKey: "deploy:prod:2026-07-24:summary",
});This shape gives an approval UI something concrete to show. It also gives policy code a stable input: warnings, changed fields, target, provider, and revision—not a vague natural-language promise about what the agent intends to do.
Provider reality
One safety contract, five different APIs
“Issue tracker” sounds like one category, but the write models are not equivalent. A portable layer should expose the differences it cannot safely erase.
| Provider | Concurrency strategy | Translation risk |
|---|---|---|
| GitHub Issues | Re-read and compare the normalized revision before mutation | State is open/closed while project workflows may live elsewhere |
| GitLab | Re-read and compare before mutation | Unknown labels can become new project labels unless writes are guarded |
| Linear | Re-read and compare update timestamps | States are team-specific identifiers, not portable names |
| Jira Cloud | Re-read plus provider version checks where available | Status changes are transitions; fields depend on project configuration |
| Azure DevOps | Atomic JSON Patch revision test | Work-item types, states, and fields depend on the process template |
The normalized API should therefore include capability discovery and warnings. When an adapter cannot preserve a requested meaning, the caller should see that before commit. For complete access to every provider-specific endpoint, the official provider SDK is still the right tool.
The protocol
Six rules for safe agent writes
- Prepare from current state.Resolve normalized intent against the real item and provider capabilities.
- Make the diff inspectable.Return exact field changes and warnings before any mutation.
- Fingerprint the plan.Reject a plan if its reviewed input, target, warnings, or revision changed.
- Bind idempotency to intent.The same key and same intent replays; the same key with different intent conflicts.
- Check the revision.Fail closed when a person or another agent changed the item after preparation.
- Verify the result.Return a receipt and reconcile uncertain provider outcomes before inventing another write.
A durable idempotency store is essential in serverless and multi-process systems. The key should identify the business event—webhook delivery, deployment, approval, or job—not the individual retry attempt.
Run it locally
Try the failure boundary without credentials
Work SDK includes a deterministic memory adapter, so the safe-write lifecycle can be tested before connecting a real organization or repository.
import { createWorkClient } from "work-sdk";
import { memoryWorkAdapter, workItemFixture } from "work-sdk/testing";
const adapter = memoryWorkAdapter({
items: [workItemFixture({ id: "42", identifier: "DEMO-42" })],
});
const work = createWorkClient({ adapter });
const change = await work.prepareComment("DEMO-42", {
body: "Deployment verified. All checks passed.",
});
const first = await work.commit(change, { idempotencyKey: "deploy:42" });
const replay = await work.commit(change, { idempotencyKey: "deploy:42" });
console.log(first.replayed); // false
console.log(replay.replayed); // true — no second provider writeStart with fake data, then switch one adapter when the approval flow is ready.