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.

Two independent questions

“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.

ProviderConcurrency strategyTranslation risk
GitHub IssuesRe-read and compare the normalized revision before mutationState is open/closed while project workflows may live elsewhere
GitLabRe-read and compare before mutationUnknown labels can become new project labels unless writes are guarded
LinearRe-read and compare update timestampsStates are team-specific identifiers, not portable names
Jira CloudRe-read plus provider version checks where availableStatus changes are transitions; fields depend on project configuration
Azure DevOpsAtomic JSON Patch revision testWork-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

  1. Prepare from current state.Resolve normalized intent against the real item and provider capabilities.
  2. Make the diff inspectable.Return exact field changes and warnings before any mutation.
  3. Fingerprint the plan.Reject a plan if its reviewed input, target, warnings, or revision changed.
  4. Bind idempotency to intent.The same key and same intent replays; the same key with different intent conflicts.
  5. Check the revision.Fail closed when a person or another agent changed the item after preparation.
  6. 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 write
Build the first integration in five minutes.

Start with fake data, then switch one adapter when the approval flow is ready.