{"record":{"id":"7aa69c69241a1c9b","repo":"different-ai/openwork","slug":"automation-revision-changed","errorCode":"automation_revision_changed","errorMessage":"automation_revision_changed","messagePattern":"automation_revision_changed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"ee/apps/den-api/src/automations/repository.ts","lineNumber":541,"sourceCode":"    organizationId: string\n    ownerMemberId: string\n    automation: Automation\n    revision: AutomationRevision\n    nonce: string\n    code: AutomationError[\"code\"]\n    message: string\n    now: number\n  }): Promise<AutomationRun> {\n    return db.transaction(async (tx) => {\n      const automationId = normalizeAutomationId(input.automation.id)\n      const revisionId = normalizeRevisionId(input.revision.id)\n      const locked = await tx.select().from(AutomationTable).where(and(\n        eq(AutomationTable.id, automationId),\n        eq(AutomationTable.organization_id, normalizeOrganizationId(input.organizationId)),\n        eq(AutomationTable.owner_member_id, normalizeMemberId(input.ownerMemberId)),\n      )).limit(1).for(\"update\")\n      if (!locked[0] || locked[0].state === \"archived\") throw new Error(\"automation_not_found\")\n      if (locked[0].current_revision_id !== revisionId) throw new Error(\"automation_revision_changed\")\n\n      const identity = automationOccurrenceIdentity({\n        automationId: input.automation.id,\n        scheduledFor: null,\n        nonce: input.nonce,\n      })\n      const runId = createDenTypeId(\"automationRun\")\n      await tx.insert(AutomationRunTable).values({\n        id: runId,\n        automation_id: automationId,\n        revision_id: revisionId,\n        trigger: \"manual\",\n        scheduled_for: null,\n        idempotency_key: identity.idempotencyKey,\n        status: \"skipped\",\n        execution_target: input.revision.executionTarget ?? \"desktop\",\n        claim_deadline_at: null,\n        lease_owner: null,","sourceCodeStart":523,"sourceCodeEnd":559,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-api/src/automations/repository.ts#L523-L559","documentation":"DenAutomationRepository.recordSkippedManual locks the automation row FOR UPDATE inside a transaction and verifies that the revision the caller passed still matches the row's current_revision_id. If the automation was edited (or a new revision was published) between the caller reading revisionId and recording the skip, this error aborts the transaction so a stale occurrence is never recorded against a superseded revision.","triggerScenarios":"Calling recordSkippedManual with a revisionId that no longer equals locked[0].current_revision_id — i.e. the automation was updated/republished after the manual-run occurrence was captured.","commonSituations":"A user edits the automation while a previously scheduled/manual occurrence is being acknowledged as skipped; a race between an editor publishing a new revision and a worker recording skips for the old revision; retrying an old request after the automation changed.","solutions":["Re-fetch the automation's current revision via the repository/API and re-issue recordSkippedManual with the current revisionId","If the occurrence belongs to an old revision intentionally, drop the stale occurrence instead of recording the skip","Use optimistic concurrency on the caller side: capture both automation id and revision id together right before the call"],"exampleFix":"// before\nawait repo.recordSkippedManual({ ...input, revisionId: staleRevisionId })\n// after\nconst current = await repo.automationById(input.automationId)\nawait repo.recordSkippedManual({ ...input, revisionId: current.currentRevisionId })","handlingStrategy":"validation","validationCode":"const current = await repo.automationById(automationId)\nif (current.currentRevisionId !== revisionId) {\n  throw new Error('stale revision: re-fetch before recordSkippedManual')\n}","typeGuard":"function isCurrentRevision(current: { currentRevisionId: string }, revisionId: string): boolean {\n  return current.currentRevisionId === revisionId\n}","tryCatchPattern":"try {\n  await repo.recordSkippedManual(input)\n} catch (e) {\n  if (String((e as Error)?.message) === 'automation_revision_changed') {\n    // refresh revision and retry once, or drop the stale occurrence\n  } else throw e\n}","preventionTips":["Always load the automation and its current revision atomically with the decision to skip","Avoid long delays between reading revisionId and calling recordSkippedManual","Make occurrence payloads carry the revision they belong to and discard stale ones"],"tags":["concurrency","optimistic-locking","automations"],"backgroundTag":"stale-revision-conflict","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}