Jobs

Background Jobs

Register typed background jobs, choose a backend, enqueue work, and inspect job state.

By Guillermo Alvarez - Published - Updated

Define jobs

GoLazy jobs are typed Go structs with JSON payloads. A job has a stable kind and a Work method:

package importwhatsapp

import (
    "context"

    "sample_app/app/jobs/basejob"
)

type Job struct {
    basejob.BaseJob
    ImportID int64 `json:"import_id"`
}

func (*Job) Kind() string { return "imports.whatsapp" }

func (j *Job) Work(ctx context.Context) error {
    // Load services from ctx and process j.ImportID.
    return nil
}

Put the application base job in app/jobs/basejob/basejob.go:

package basejob

import "golazy.dev/lazyjobs"

type BaseJob struct {
    lazyjobs.BaseJob
}

The app base package keeps concrete job packages from importing the registry package and avoids Go import cycles.

Register jobs

Register jobs from app/jobs/jobs.go:

package jobs

import (
    "golazy.dev/lazyjobs"
    "sample_app/app/jobs/importwhatsapp"
)

func DefinedJobs(runner *lazyjobs.JobRunner) {
    runner.MustRegister(&importwhatsapp.Job{})
}

Wire the registry into lazyapp.Config.Jobs:

lazyapp.New(lazyapp.Config{
    Name:         "sample_app",
    Drawer:       Draw,
    Public:       app.Public,
    Views:        app.Views,
    Dependencies: Dependencies,
    Jobs: lazyapp.Jobs(lazyjobs.Config{
        Define: jobs.DefinedJobs,
    }),
})

Jobs runs after Dependencies with the dependency-initialized app context. When no backend is passed, GoLazy uses the bundled in-memory backend.

Backend interface

Durability is behind lazyjobs.Backend:

type Backend interface {
    Insert(context.Context, lazyjobs.InsertParams) (lazyjobs.Record, error)
    Claim(context.Context, lazyjobs.ClaimParams) (lazyjobs.Record, bool, error)
    Complete(context.Context, int64) error
    Retry(context.Context, lazyjobs.RetryParams) error
    Discard(context.Context, lazyjobs.DiscardParams) error
    List(context.Context, lazyjobs.ListOptions) ([]lazyjobs.Record, error)
    Stats(context.Context) (lazyjobs.Stats, error)
}

Insert stores pending work. Claim must atomically select one due job from the requested queues and move it to running. Complete, Retry, and Discard record runner decisions after Work returns or panics. List and Stats power operational views.

The backend owns persistence and concurrency. The runner owns decoding, calling Work, retry-delay decisions, and state-transition calls.

Implement a backend

A backend should:

  1. Store Kind, Queue, JSON Payload, State, attempts, RunAt, timestamps, and LastError.
  2. Default empty queues to lazyjobs.DefaultQueue.
  3. Claim only pending or retrying records whose RunAt is due.
  4. Make claim atomic so two workers cannot run the same record.
  5. Preserve payload bytes exactly enough for registered job decoding.
  6. Return newest recent records from List.
  7. Return aggregate counts by state, kind, and queue from Stats.

If the backend owns network connections or goroutines, implement Close() error; the runner calls it during shutdown when present.

Built-in backends

golazy.dev/lazyjobs/inmemoryjobs is the default in-process backend. It is useful for development, tests, and apps where queued work does not need to survive process restarts.

PostgreSQL apps can use golazy.dev/pg/pgjobs. Include pgjobs.Migrations() in the migration catalog and build the job config from context after dependencies initialize. Read PostgreSQL.

Enqueue work

Resolve the runner from the request or service context:

runner, ok := lazyjobs.RunnerFromContext(ctx)
if !ok {
    return errors.New("jobs are not configured")
}

_, err := runner.Enqueue(ctx, &importwhatsapp.Job{ImportID: importID})
return err

The runner serializes the job payload to JSON, stores it through the backend, and starts in-process workers with the application.

Use EnqueueIn or EnqueueAt for delayed work.

Inspect jobs

When jobs are configured, GoLazy registers read-only GET /jobs on the app control plane. It returns runner status, job definitions, state counts, and recent jobs. In lazy development, the GoLazy panel proxies that endpoint and shows the same data in the Jobs tab.

Production builds still follow normal control-plane exposure rules: use CONTROL_PLANE_ADDR when job state should be available outside application routes.