hashicorp/nomad · error

cannot submit plan without job info

Error message

cannot submit plan without job info

What it means

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.

Source

Thrown at nomad/plan_endpoint.go:48

	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)
		}
		plan.Job = job
	}

	// Pause the Nack timer for the eval as it is making progress as long as it
	// is in the plan queue. We resume immediately after we get a result to

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Set plan.Job to the full job struct before Submit
  2. Or set plan.JobInfo with at least Namespace and ID so the handler can look the job up
  3. Upgrade all servers/workers to matching Nomad versions so JobInfo is populated consistently
  4. Fix the constructing code path so it never sends a Plan without job identity

Example fix

// before
plan := &structs.Plan{}
// after
plan := &structs.Plan{Job: job, JobInfo: structs.PlanJobInfo{ID: job.ID, Namespace: job.Namespace}}
Defensive patterns

Strategy: validation

Validate before calling

if plan.Job == nil && plan.JobInfo == nil {
    return fmt.Errorf("plan needs Job or JobInfo (Namespace, ID)")
}

Type guard

func planHasJobIdentity(p *structs.Plan) bool {
    return p != nil && (p.Job != nil || (p.JobInfo != nil && p.JobInfo.ID != ""))
}

Try / catch

if err := submitPlan(plan); err != nil && strings.Contains(err.Error(), "without job info") {
    return fmt.Errorf("plan lacked job identity: %w", err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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