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

  1. Ensure the caller populates args.Plan with a valid *structs.Plan before Submit
  2. If only JobInfo is available, populate plan.JobInfo (namespace + ID) instead of plan.Job
  3. Fix the internal code path that constructs the empty plan request
  4. 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

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


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/0282223e08e4ca69. Report an issue: GitHub.