hashicorp/nomad · error
cannot submit nil plan
Error message
cannot submit nil plan
What it means
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.
Source
Thrown at nomad/plan_endpoint.go:42
return &Plan{srv: srv, ctx: ctx, logger: srv.logger.Named("plan")}
}
// Submit is used to submit a plan to the leader
func (p *Plan) Submit(args *structs.PlanRequest, reply *structs.PlanResponse) error {
aclObj, err := p.srv.AuthenticateServerOnly(p.ctx, args)
p.srv.MeasureRPCRate("plan", structs.RateMetricWrite, args)
if err != nil || !aclObj.AllowServerOp() {
return structs.ErrPermissionDenied
}
if done, err := p.srv.forward("Plan.Submit", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "plan", "submit"}, time.Now())
if args.Plan == nil {
return fmt.Errorf("cannot submit nil plan")
}
plan := args.Plan
if plan.Job == nil {
if plan.JobInfo == nil {
return fmt.Errorf("cannot submit plan without job info")
}
// we lookup the job immediately after the plan submission is requested,
// in order to save time not having to look it up whenever needed and
// more importantly, to avoid nil jobs in the plan in situations when
// job gets dropped from the state store while plan is still in flight.
job, err := p.srv.State().JobByID(nil, plan.JobInfo.Namespace, plan.JobInfo.ID)
if err != nil {
return err
}
if job == nil {
return fmt.Errorf("job %q in namespace %q not found", plan.JobInfo.ID, plan.JobInfo.Namespace)View on GitHub (pinned to 482b49bf1a)
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
Example fix
// before
req := &structs.PlanSubmissionRequest{}
// after
req := &structs.PlanSubmissionRequest{Plan: &structs.Plan{JobInfo: structs.PlanJobInfo{ID: job.ID, Namespace: job.Namespace}}} Defensive patterns
Strategy: validation
Validate before calling
if req.Plan == nil {
return fmt.Errorf("plan required before Plan.Submit")
} Type guard
func hasPlan(req *structs.PlanSubmissionRequest) bool { return req != nil && req.Plan != nil } Try / catch
if err := submitPlan(plan); err != nil && strings.Contains(err.Error(), "cannot submit nil plan") {
return fmt.Errorf("caller bug: Plan was not set: %w", err)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- cannot submit plan without job info
- job %q in namespace %q not found
- missing region for target RPC
- no servers
- missing NodeID
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/0282223e08e4ca69.
Report an issue: GitHub.