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 toView on GitHub (pinned to 482b49bf1a)
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
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
- 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
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
- cannot submit nil plan
- 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/9b3d94d98507da1e.
Report an issue: GitHub.