CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-go-co-op-gocron-v2

A Golang job scheduling library that lets you run Go functions at pre-determined intervals using cron expressions, fixed durations, daily, weekly, monthly, or one-time schedules with support for distributed deployments.

Overview
Eval results
Files

managing-jobs.mddocs/guides/jobs/

Managing Jobs

How to update, remove, query job status, and trigger jobs manually with RunNow.

Querying Job Status

Job ID and Metadata

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithName("my-job"),
    gocron.WithTags("worker", "cleanup"),
)

// Get job metadata
id := j.ID()           // uuid.UUID
name := j.Name()       // string
tags := j.Tags()       // []string

Last Run Time

func (j Job) LastRun() (time.Time, error)

Returns the time of the last execution:

lastRun, err := j.LastRun()
if errors.Is(err, gocron.ErrJobNotFound) {
    log.Println("Job was removed")
} else if err != nil {
    log.Printf("Error: %v", err)
} else {
    fmt.Printf("Last run: %v\n", lastRun)
}

Returns ErrJobNotFound if the job was removed.

Next Run Time

func (j Job) NextRun() (time.Time, error)
func (j Job) NextRuns(count uint) ([]time.Time, error)

Query upcoming execution times:

// Next scheduled run
nextRun, err := j.NextRun()
if err == nil {
    fmt.Printf("Next run: %v\n", nextRun)
}

// Next 5 scheduled runs
nextFive, err := j.NextRuns(5)
if err == nil {
    for i, t := range nextFive {
        fmt.Printf("Run %d: %v\n", i+1, t)
    }
}

List All Jobs

func (s Scheduler) Jobs() []Job

Returns all registered jobs, sorted by ID:

allJobs := s.Jobs()
for _, j := range allJobs {
    fmt.Printf("Job: %s (%s)\n", j.Name(), j.ID())
    nextRun, _ := j.NextRun()
    fmt.Printf("  Next run: %v\n", nextRun)
    fmt.Printf("  Tags: %v\n", j.Tags())
}

Manual Triggering

RunNow

func (j Job) RunNow() error

Triggers job immediately without affecting schedule:

j, _ := s.NewJob(
    gocron.DailyJob(1, gocron.NewAtTimes(gocron.NewAtTime(9, 0, 0))),
    gocron.NewTask(doReport),
    gocron.WithName("daily-report"),
)

// Normal schedule: runs at 9 AM
// Manual trigger: run now (doesn't affect 9 AM schedule)
err := j.RunNow()
if errors.Is(err, gocron.ErrJobRunNowFailed) {
    log.Println("Failed to trigger job")
}

Behavior:

  • Executes immediately (as soon as executor dispatches it)
  • Does not affect normal schedule
  • Respects concurrency limits (singleton mode, scheduler limits)
  • Returns ErrJobRunNowFailed if scheduler is not reachable

Use cases:

  • Admin-triggered actions
  • API-triggered jobs
  • Testing
  • Emergency runs

RunNow with Concurrency Limits

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() {
        time.Sleep(2 * time.Minute) // Long-running
    }),
    gocron.WithSingletonMode(gocron.LimitModeReschedule),
)

// If job is already running, RunNow respects singleton mode
err := j.RunNow()
// May skip if already running (LimitModeReschedule)

Updating Jobs

Update

func (s Scheduler) Update(
    id uuid.UUID,
    jobDefinition JobDefinition,
    task Task,
    jobOptions ...JobOption,
) (Job, error)

Replaces job definition, task, and options:

j, _ := s.NewJob(
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithName("my-job"),
)

// Later: change interval to 10 minutes
j, err := s.Update(
    j.ID(),
    gocron.DurationJob(10*time.Minute), // new interval
    gocron.NewTask(myFunc),              // same or new task
    gocron.WithName("my-job"),           // re-specify all options
)
if err != nil {
    log.Printf("Update failed: %v", err)
}

Behavior:

  • Job ID remains the same
  • Next run time is recalculated
  • Running execution is not interrupted
  • All options must be re-specified (no merging)

Update Task Only

// Change task function
j, _ = s.Update(
    j.ID(),
    gocron.DurationJob(5*time.Minute), // keep same schedule
    gocron.NewTask(myNewFunc),         // new task
    gocron.WithName("my-job"),
)

Update Schedule Only

// Change schedule, keep task
j, _ = s.Update(
    j.ID(),
    gocron.DurationJob(10*time.Minute), // new schedule
    gocron.NewTask(myFunc),              // keep same task
    gocron.WithName("my-job"),
)

Update Options

// Add singleton mode
j, _ = s.Update(
    j.ID(),
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithName("my-job"),
    gocron.WithSingletonMode(gocron.LimitModeReschedule), // new option
)

Update Errors

Returns ErrJobNotFound if the job doesn't exist:

_, err := s.Update(
    uuid.New(), // non-existent ID
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
)
if errors.Is(err, gocron.ErrJobNotFound) {
    log.Println("Job not found")
}

Removing Jobs

RemoveJob

func (s Scheduler) RemoveJob(id uuid.UUID) error

Removes a job by ID:

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
)

// Later: remove the job
err := s.RemoveJob(j.ID())
if errors.Is(err, gocron.ErrJobNotFound) {
    log.Println("Job already removed")
}

Behavior:

  • Job is removed from scheduler
  • Running execution is not interrupted
  • Future runs are cancelled
  • Returns ErrJobNotFound if job doesn't exist

RemoveByTags

func (s Scheduler) RemoveByTags(tags ...string)

Removes all jobs with at least one of the specified tags:

// Add jobs with tags
s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(task1),
    gocron.WithTags("cleanup", "database"),
)

s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(task2),
    gocron.WithTags("cleanup", "files"),
)

s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(task3),
    gocron.WithTags("reporting"),
)

// Remove all cleanup jobs (removes task1 and task2)
s.RemoveByTags("cleanup")

// Remove jobs with "temporary" OR "test" tag
s.RemoveByTags("temporary", "test")

Tag matching: A job is removed if it has ANY of the specified tags (OR logic).

Automatic Removal

Jobs are automatically removed when:

  1. Limited runs exhausted:
j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithLimitedRuns(3), // Removed after 3 runs
)
  1. Stop time reached:
end := time.Now().Add(24 * time.Hour)
j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithStopAt(gocron.WithStopDateTime(end)), // Removed after 24h
)
  1. OneTimeJob completes:
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(myFunc), // Removed after single execution
)

Queue Monitoring

JobsWaitingInQueue

func (s Scheduler) JobsWaitingInQueue() int

Returns the number of jobs queued due to concurrency limits:

s, _ := gocron.NewScheduler(
    gocron.WithLimitConcurrentJobs(3, gocron.LimitModeWait),
)

// Monitor queue size
go func() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        waiting := s.JobsWaitingInQueue()
        if waiting > 10 {
            log.Printf("Warning: %d jobs waiting", waiting)
        }
    }
}()

See Concurrency: Scheduler Limits for details.

Advanced Patterns

Job Registry Pattern

Track jobs for later management:

type JobRegistry struct {
    jobs map[string]gocron.Job
    mu   sync.RWMutex
}

func (r *JobRegistry) Register(name string, job gocron.Job) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.jobs[name] = job
}

func (r *JobRegistry) Get(name string) (gocron.Job, bool) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    job, ok := r.jobs[name]
    return job, ok
}

func (r *JobRegistry) TriggerByName(name string) error {
    job, ok := r.Get(name)
    if !ok {
        return fmt.Errorf("job %s not found", name)
    }
    return job.RunNow()
}

// Usage
registry := &JobRegistry{jobs: make(map[string]gocron.Job)}

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithName("my-job"),
)
registry.Register("my-job", j)

// Later: trigger by name
registry.TriggerByName("my-job")

Dynamic Job Updates

// Update job based on config changes
func updateJobFromConfig(s gocron.Scheduler, jobID uuid.UUID, config Config) error {
    var interval time.Duration
    switch config.Frequency {
    case "fast":
        interval = time.Minute
    case "normal":
        interval = 5 * time.Minute
    case "slow":
        interval = 15 * time.Minute
    default:
        return fmt.Errorf("invalid frequency: %s", config.Frequency)
    }

    _, err := s.Update(
        jobID,
        gocron.DurationJob(interval),
        gocron.NewTask(myFunc),
        gocron.WithName(config.Name),
    )
    return err
}

Conditional Job Removal

// Remove jobs based on criteria
func removeInactiveJobs(s gocron.Scheduler) {
    for _, j := range s.Jobs() {
        lastRun, err := j.LastRun()
        if err != nil {
            continue
        }

        // Remove if not run in 24 hours
        if time.Since(lastRun) > 24*time.Hour {
            log.Printf("Removing inactive job: %s", j.Name())
            s.RemoveJob(j.ID())
        }
    }
}

Job Status Dashboard

func printJobStatus(s gocron.Scheduler) {
    jobs := s.Jobs()
    fmt.Printf("Total jobs: %d\n", len(jobs))
    fmt.Printf("Jobs waiting in queue: %d\n", s.JobsWaitingInQueue())
    fmt.Println("\nJob Details:")

    for _, j := range jobs {
        fmt.Printf("  %s (ID: %s)\n", j.Name(), j.ID())
        fmt.Printf("    Tags: %v\n", j.Tags())

        lastRun, err := j.LastRun()
        if err == nil {
            fmt.Printf("    Last run: %v (%v ago)\n", lastRun, time.Since(lastRun))
        }

        nextRun, err := j.NextRun()
        if err == nil {
            fmt.Printf("    Next run: %v (in %v)\n", nextRun, time.Until(nextRun))
        }
    }
}

Best Practices

1. Store Job IDs for Updates

// Good: store ID for later updates
jobID := j.ID()
saveJobID(jobID) // persist if needed

// Later: update using stored ID
s.Update(jobID, newDefinition, newTask)

2. Use Named Jobs for Management

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(myFunc),
    gocron.WithName("my-job"), // Easy to identify
)

3. Tag Jobs for Bulk Operations

// Tag temporary jobs
s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(task1),
    gocron.WithTags("temporary", "experiment"),
)

// Later: remove all temporary jobs
s.RemoveByTags("temporary")

4. Check Errors When Querying

nextRun, err := j.NextRun()
if errors.Is(err, gocron.ErrJobNotFound) {
    log.Println("Job was removed")
} else if err != nil {
    log.Printf("Error: %v", err)
} else {
    fmt.Printf("Next run: %v\n", nextRun)
}

5. Monitor Queue Size

go func() {
    ticker := time.NewTicker(1 * time.Minute)
    defer ticker.Stop()

    for range ticker.C {
        waiting := s.JobsWaitingInQueue()
        if waiting > 50 {
            log.Printf("Queue backlog: %d jobs", waiting)
        }
    }
}()

Troubleshooting

Job Not Found Errors

Symptom: ErrJobNotFound when querying or updating job.

Causes:

  • Job was removed (manually or automatically)
  • Job completed with WithLimitedRuns
  • Job stop time reached
  • OneTimeJob finished

Solution: Check if job still exists before operations:

jobs := s.Jobs()
found := false
for _, job := range jobs {
    if job.ID() == myJobID {
        found = true
        break
    }
}

if !found {
    log.Println("Job no longer exists")
}

RunNow Not Working

Symptom: RunNow() returns error or doesn't execute.

Causes:

  • Scheduler not started
  • Concurrency limit reached
  • Job already running (singleton mode)

Solution: Check scheduler state and concurrency:

if err := j.RunNow(); err != nil {
    log.Printf("RunNow failed: %v", err)
    log.Printf("Jobs waiting: %d", s.JobsWaitingInQueue())
}

Update Not Taking Effect

Symptom: Job continues with old schedule/task.

Cause: Running execution not interrupted.

Solution: Wait for current execution to complete:

s.Update(j.ID(), newDefinition, newTask)
// Next run will use new definition

Related Documentation

Install with Tessl CLI

npx tessl i tessl/golang-github-com-go-co-op-gocron-v2@2.19.1

docs

index.md

tile.json