{"record":{"id":"3123c23ecbe1f60b","repo":"gatsbyjs/gatsby","slug":"the-plugin-get-action-plugin-name-anonym","errorCode":null,"errorMessage":"The plugin \"${_.get(action, `plugin.name`, `anonymous`)}\" tried to end a job with the id \"${action.payload.id}\" that either hasn't yet been created or has already been ended","messagePattern":"The plugin \"(.+?)\" tried to end a job with the id \"(.+?)\" that either hasn't yet been created or has already been ended","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/gatsby/src/redux/reducers/jobs.ts","lineNumber":43,"sourceCode":"        state.active[index] = mergedJob\n        return state\n      } else {\n        state.active.push({\n          ...action.payload,\n          createdAt: Date.now(),\n          plugin: action.plugin,\n        })\n        return state\n      }\n    }\n    case `END_JOB`: {\n      if (!action.payload.id) {\n        throw new Error(`An ID must be provided when ending a job`)\n      }\n      const completedAt = Date.now()\n      const index = _.findIndex(state.active, j => j.id === action.payload.id)\n      if (index === -1) {\n        throw new Error(oneLine`\n          The plugin \"${_.get(action, `plugin.name`, `anonymous`)}\"\n          tried to end a job with the id \"${action.payload.id}\"\n          that either hasn't yet been created or has already been ended`)\n      }\n      const job = state.active.splice(index, 1)[0]\n      state.done.push({\n        ...job,\n        completedAt,\n        runTime: moment(completedAt).diff(moment(job.createdAt)),\n      })\n\n      return state\n    }\n  }\n\n  return state\n}\n","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/gatsbyjs/gatsby/blob/8b06340921ffdf23125a365b9c9923690cb62ce6/packages/gatsby/src/redux/reducers/jobs.ts#L25-L61","documentation":"The v1 jobs reducer's END_JOB case looks up action.payload.id in state.jobs.active via findIndex; if no active job has that id it throws, because ending a job that was never created (or was already ended) indicates a lifecycle bug. The message names the offending plugin (defaulting to 'anonymous') and the offending id. This guards against double-end and end-without-create.","triggerScenarios":"Dispatching END_JOB for an id that is not in state.jobs.active: either the job was never created via CREATE_JOB/SET_JOB, or it was already ended by a prior END_JOB (active is mutated via splice so a second end misses).","commonSituations":"Calling endJob twice for the same job; ending a job whose create failed or was skipped; plugin lifecycle mismatch where onPostBootstrap ends a job started in a different (skipped) hook; cached/replayed job state after a partial build.","solutions":["Ensure every endJob is preceded by a successful createJob/setJob with the same id.","Guard against double-end by tracking local 'ended' state per job id in your plugin.","Use stable, content-digest-based ids so re-runs do not end already-completed jobs.","Switch to the v2 job API which de-duplicates by content digest."],"exampleFix":"// before\nactions.endJob({ id: jobId })\n// may run again on rebuild ->\n// after\nconst ended = new Set()\nfunction finish(id) {\n  if (ended.has(id)) return\n  ended.add(id)\n  actions.endJob({ id })\n}","handlingStrategy":"try-catch","validationCode":"// Track active job ids locally to avoid double-end / end-without-create.\nconst active = new Set()\nfunction start(actions, id) { active.add(id); actions.createJob({ id }) }\nfunction finish(actions, id) {\n  if (!active.has(id)) return // never created or already ended\n  active.delete(id)\n  actions.endJob({ id })\n}","typeGuard":"function isActiveJob(state, id) {\n  return state.jobs.active.some(j => j.id === id)\n}","tryCatchPattern":"try {\n  actions.endJob({ id })\n} catch (e) {\n  if (/tried to end a job/.test(e.message)) {\n    // already ended or never started - safe to ignore, mark locally\n  } else throw e\n}","preventionTips":["Maintain a local Set of pending job ids and delete on end.","Use content-digest ids so repeated work collapses to the same job.","Guard endJob in lifecycle hooks that may run more than once."],"tags":["jobs","reducer","lifecycle","plugin-api"],"backgroundTag":null,"analyzedSha":"8b06340921ffdf23125a365b9c9923690cb62ce6","analyzedAt":"2026-08-13T02:36:21.405Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}