{"record":{"id":"e2d2a09662de475f","repo":"hashicorp/nomad","slug":"deployment-q-is-terminal","errorCode":null,"errorMessage":"deployment %q is terminal","messagePattern":"deployment %q is terminal","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/deploymentwatcher/deployments_watcher.go","lineNumber":280,"sourceCode":"\n// add adds a deployment to the watch list\nfunc (w *Watcher) add(d *structs.Deployment) error {\n\tw.l.Lock()\n\tdefer w.l.Unlock()\n\t_, err := w.addLocked(d)\n\treturn err\n}\n\n// addLocked adds a deployment to the watch list and should only be called when\n// locked. Creating the deploymentWatcher starts a go routine to .watch() it\nfunc (w *Watcher) addLocked(d *structs.Deployment) (*deploymentWatcher, error) {\n\t// Not enabled so no-op\n\tif !w.enabled {\n\t\treturn nil, nil\n\t}\n\n\tif !d.Active() {\n\t\treturn nil, fmt.Errorf(\"deployment %q is terminal\", d.ID)\n\t}\n\n\t// Already watched so just update the deployment\n\tif w, ok := w.watchers[d.ID]; ok {\n\t\tw.updateDeployment(d)\n\t\treturn nil, nil\n\t}\n\n\t// Get the job the deployment is referencing\n\tsnap, err := w.state.Snapshot()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjob, err := snap.JobByID(nil, d.Namespace, d.JobID)\n\tif err != nil {\n\t\treturn nil, err\n\t}","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/deploymentwatcher/deployments_watcher.go#L262-L298","documentation":"addLocked refuses to start a watcher for a deployment whose status is terminal (not Active()). Nomad throws this because only active deployments need live watching; requesting a watcher for a finished/cancelled/paused-to-terminal deployment is a caller bug.","triggerScenarios":"add() or forceAdd() call addLocked with a deployment d where d.Active() is false (status failed, cancelled, or successful).","commonSituations":"Re-adding watchers after leader failover for deployments that completed before the election; calling force-add/reevaluate APIs against an old deployment ID; replaying state snapshots containing terminal deployments.","solutions":["Check `nomad deployment status <id>` — if the deployment is terminal, no watcher is needed; ignore the error","Only call force-add/reevaluation on active deployments","Filter deployments by Active() before attempting to register a watcher (e.g. after leader election)","Create a new deployment by updating the job instead of trying to revive a terminal one"],"exampleFix":"// before\nw.addLocked(dep)\n// after\nif dep.Active() {\n    w.addLocked(dep)\n}\n","handlingStrategy":"validation","validationCode":"// guard before re-adding a watcher\nd, err := state.DeploymentByID(nil, id)\nif err == nil && d != nil && d.Active() {\n    watcher.Add(d) // safe\n}","typeGuard":"func canWatch(d *structs.Deployment) bool { return d != nil && d.Active() }","tryCatchPattern":"try {\n    watcher.add(dep)\n} catch (e) {\n    if (e.message.includes('is terminal')) {\n        // skip: deployment finished; no action needed\n        return null\n    }\n    throw e\n}","preventionTips":["Filter to active deployments after leader election before re-adding watchers","Never force-add/reevaluate terminal deployment IDs","Check deployment status before calling promotion/unblock APIs"],"tags":["nomad","deployment","lifecycle"],"backgroundTag":"deployment-already-terminal","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}