{"record":{"id":"9b3d94d98507da1e","repo":"hashicorp/nomad","slug":"cannot-submit-plan-without-job-info","errorCode":null,"errorMessage":"cannot submit plan without job info","messagePattern":"cannot submit plan without job info","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/plan_endpoint.go","lineNumber":48,"sourceCode":"\taclObj, err := p.srv.AuthenticateServerOnly(p.ctx, args)\n\tp.srv.MeasureRPCRate(\"plan\", structs.RateMetricWrite, args)\n\tif err != nil || !aclObj.AllowServerOp() {\n\t\treturn structs.ErrPermissionDenied\n\t}\n\n\tif done, err := p.srv.forward(\"Plan.Submit\", args, args, reply); done {\n\t\treturn err\n\t}\n\tdefer metrics.MeasureSince([]string{\"nomad\", \"plan\", \"submit\"}, time.Now())\n\n\tif args.Plan == nil {\n\t\treturn fmt.Errorf(\"cannot submit nil plan\")\n\t}\n\n\tplan := args.Plan\n\tif plan.Job == nil {\n\t\tif plan.JobInfo == nil {\n\t\t\treturn fmt.Errorf(\"cannot submit plan without job info\")\n\t\t}\n\n\t\t// we lookup the job immediately after the plan submission is requested,\n\t\t// in order to save time not having to look it up whenever needed and\n\t\t// more importantly, to avoid nil jobs in the plan in situations when\n\t\t// job gets dropped from the state store while plan is still in flight.\n\t\tjob, err := p.srv.State().JobByID(nil, plan.JobInfo.Namespace, plan.JobInfo.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif job == nil {\n\t\t\treturn fmt.Errorf(\"job %q in namespace %q not found\", plan.JobInfo.ID, plan.JobInfo.Namespace)\n\t\t}\n\t\tplan.Job = job\n\t}\n\n\t// Pause the Nack timer for the eval as it is making progress as long as it\n\t// is in the plan queue. We resume immediately after we get a result to","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/plan_endpoint.go#L30-L66","documentation":"A submitted plan must identify the job it belongs to. When plan.Job is nil, the handler falls back to plan.JobInfo; if that is also nil, there is no way to resolve the job and the RPC is rejected. This prevents nil jobs in plans and needless state-store lookups.","triggerScenarios":"Plan.Submit called with a Plan whose Job and JobInfo fields are both nil — malformed internal plan submission from scheduler/worker code or custom tooling.","commonSituations":"Older worker code after a version upgrade that introduced the JobInfo requirement; handcrafted RPCs in tests or forks; partially initialized Plan structs in custom autoscaler-like integrations.","solutions":["Set plan.Job to the full job struct before Submit","Or set plan.JobInfo with at least Namespace and ID so the handler can look the job up","Upgrade all servers/workers to matching Nomad versions so JobInfo is populated consistently","Fix the constructing code path so it never sends a Plan without job identity"],"exampleFix":"// before\nplan := &structs.Plan{}\n// after\nplan := &structs.Plan{Job: job, JobInfo: structs.PlanJobInfo{ID: job.ID, Namespace: job.Namespace}}","handlingStrategy":"validation","validationCode":"if plan.Job == nil && plan.JobInfo == nil {\n    return fmt.Errorf(\"plan needs Job or JobInfo (Namespace, ID)\")\n}","typeGuard":"func planHasJobIdentity(p *structs.Plan) bool {\n    return p != nil && (p.Job != nil || (p.JobInfo != nil && p.JobInfo.ID != \"\"))\n}","tryCatchPattern":"if err := submitPlan(plan); err != nil && strings.Contains(err.Error(), \"without job info\") {\n    return fmt.Errorf(\"plan lacked job identity: %w\", err)\n}","preventionTips":["Populate both Job and JobInfo when building plans","Keep server/worker versions aligned to avoid missing JobInfo wiring","Validate plan construction in scheduler unit tests"],"tags":["nomad","rpc","plan","internal-api"],"backgroundTag":"plan-missing-job-info","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"}