sipeed/picoclaw · error
job not found
Error message
job not found
What it means
UpdateJob iterates cs.store.Jobs looking for a job whose ID equals job.ID; if none matches it returns this error without modifying anything. It is the library's not-found sentinel for the update path. Note it also fires when Start() was never called (store is nil/empty) and after a one-shot 'at' job auto-deleted itself (DeleteAfterRun).
Source
Thrown at pkg/cron/service.go:487
previous := cs.store.Jobs[i]
updated := cloneCronJob(*job)
now := time.Now().UnixMilli()
updated.UpdatedAtMS = now
if updated.Enabled {
if previous.Enabled != updated.Enabled || !sameSchedule(previous.Schedule, updated.Schedule) {
updated.State.NextRunAtMS = cs.computeNextRun(&updated.Schedule, now)
}
} else {
updated.State.NextRunAtMS = nil
}
cs.store.Jobs[i] = updated
cs.notify()
return cs.saveStoreUnsafe()
}
}
return fmt.Errorf("job not found")
}
func cloneCronJob(job CronJob) CronJob {
clone := job
if job.Schedule.AtMS != nil {
atMS := *job.Schedule.AtMS
clone.Schedule.AtMS = &atMS
}
if job.Schedule.EveryMS != nil {
everyMS := *job.Schedule.EveryMS
clone.Schedule.EveryMS = &everyMS
}
if job.State.NextRunAtMS != nil {
nextRunAtMS := *job.State.NextRunAtMS
clone.State.NextRunAtMS = &nextRunAtMS
}
if job.State.LastRunAtMS != nil {
lastRunAtMS := *job.State.LastRunAtMSView on GitHub (pinned to 49183d7e8d)
Solutions
- Re-fetch the current ID right before updating: GetJob(jobID) and treat ok==false as expected (job is gone - drop it from your UI/queue)
- If the job should exist, verify you are talking to the same service/store: check storePath and ListJobs output
- For one-shot 'at' jobs, expect this error after execution - they delete themselves; use AddJob to reschedule
- Never reuse zero-value CronJob structs; always start from a GetJob/ListJobs copy and mutate fields you intend to change
Example fix
// before: blind update with a possibly stale copy
if err := svc.UpdateJob(&staleJob); err != nil {
return err // "job not found" surprises callers
}
// after: guard with GetJob and degrade gracefully
if _, ok := svc.GetJob(staleJob.ID); !ok {
// job removed or one-shot already fired; nothing to update
return nil
}
if err := svc.UpdateJob(&staleJob); err != nil {
if strings.Contains(err.Error(), "job not found") {
return nil // lost a race with RemoveJob - acceptable
}
return err
} Defensive patterns
Strategy: validation
Validate before calling
// Re-validate immediately before updating; narrows the race window to near zero.
if _, ok := svc.GetJob(job.ID); !ok {
// job gone (removed or one-shot 'at' already fired): nothing to update
return nil
}
if err := svc.UpdateJob(job); err != nil { ... } Type guard
func isJobNotFound(err error) bool {
return err != nil && strings.Contains(err.Error(), "job not found")
} Try / catch
if err := svc.UpdateJob(job); err != nil {
if isJobNotFound(err) {
// expected after auto-delete of one-shot jobs or a concurrent RemoveJob;
// refresh your job list rather than surfacing an error
return refreshJobs()
}
return err
} Prevention
- Always copy the job from GetJob/ListJobs immediately before UpdateJob; never cache IDs across turns
- Remember schedule.Kind=="at" jobs self-delete after running - updates after fire will 404 by design
- Treat job-not-found as an expected outcome in UIs: remove the row instead of showing an error
- Confirm the service instance/storePath matches where the job was created before assuming a bug
When it happens
Trigger: UpdateJob(&CronJob{ID: "..."}) where the ID came from a stale ListJobs snapshot and another goroutine already RemoveJob'd it; updating a schedule.Kind=="at" job that already ran and was auto-deleted; job.ID empty (zero-value struct); service restarted against a different storePath so old IDs are gone.
Common situations: UI holding a cached job list while the job is removed elsewhere; retrying an update against a restarted service; passing a job copy from a previous deployment; racing RemoveJob vs UpdateJob in concurrent handlers.
Related errors
- Failed to load config
- Request failed with status ${res.status}
- Failed to fetch config
- Failed to save config
- events: bus is closed
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/d4d9b038a113f313.
Report an issue: GitHub.