{"record":{"id":"0282223e08e4ca69","repo":"hashicorp/nomad","slug":"cannot-submit-nil-plan","errorCode":null,"errorMessage":"cannot submit nil plan","messagePattern":"cannot submit nil plan","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/plan_endpoint.go","lineNumber":42,"sourceCode":"\treturn &Plan{srv: srv, ctx: ctx, logger: srv.logger.Named(\"plan\")}\n}\n\n// Submit is used to submit a plan to the leader\nfunc (p *Plan) Submit(args *structs.PlanRequest, reply *structs.PlanResponse) error {\n\n\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)","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/plan_endpoint.go#L24-L60","documentation":"The Plan.Submit RPC requires a non-nil Plan in the request. The handler rejects the RPC early with this error when args.Plan is nil. It guards the scheduler plan-applier from nil-pointer work on the leader.","triggerScenarios":"An internal caller (worker/scheduler on a client of the plan endpoint) submits structs.PlanSubmissionRequest with Plan left unset — typically a bug in custom scheduler code or an internal API misuse, not a user-facing CLI action.","commonSituations":"Custom integrations or forks calling Plan.Submit directly; upgrades where a new required field (e.g. JobInfo) is not populated by older code paths; handcrafted RPCs in tests.","solutions":["Ensure the caller populates args.Plan with a valid *structs.Plan before Submit","If only JobInfo is available, populate plan.JobInfo (namespace + ID) instead of plan.Job","Fix the internal code path that constructs the empty plan request","Audit test harnesses / tools that build PlanSubmissionRequest manually"],"exampleFix":"// before\nreq := &structs.PlanSubmissionRequest{}\n// after\nreq := &structs.PlanSubmissionRequest{Plan: &structs.Plan{JobInfo: structs.PlanJobInfo{ID: job.ID, Namespace: job.Namespace}}}","handlingStrategy":"validation","validationCode":"if req.Plan == nil {\n    return fmt.Errorf(\"plan required before Plan.Submit\")\n}","typeGuard":"func hasPlan(req *structs.PlanSubmissionRequest) bool { return req != nil && req.Plan != nil }","tryCatchPattern":"if err := submitPlan(plan); err != nil && strings.Contains(err.Error(), \"cannot submit nil plan\") {\n    return fmt.Errorf(\"caller bug: Plan was not set: %w\", err)\n}","preventionTips":["Always construct Plan with Job or JobInfo populated","Add unit assertions that plan requests are fully built","Keep internal RPC helpers that enforce required fields"],"tags":["nomad","rpc","plan","internal-api"],"backgroundTag":"nil-plan-submission","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"}