Jobs
A Job is a named unit of queued work with a payload and a registered handler.
Jobs make background work explicit, observable, and operable. Retry policy is opt-in: a generated job does not gain application retries until dispatch sets a retry budget.
When to Use Jobs
Use a job when background work needs a stable name, typed payload, handler, retry behavior, or worker lifecycle. Start with a small payload containing IDs that let the handler load current source-of-truth data.
A direct function call is simpler for synchronous behavior. Publish an event instead when the message is a fact that subscribers may observe. Add dedicated queues, retry policy, idempotency keys, and worker allocation when the operational requirements call for them.
Generate a Job
forj make:job reports:generate --queue reportsFor an additional app, prefix the generator with the app name:
forj admin make:job reports:generate --queue reportsUse category:action for job names, such as emails:send or reports:generate. See Naming Conventions for the full naming map.
Implement the Job
The scaffold supplies dispatch and handler seams. Replace its placeholder payload with the smallest source-of-truth references the worker needs.
GenerateJobTypeName remains an explicit constant because queue registration and transport use a stable string identifier. Go cannot derive a package-level constant from the payload struct type, and reflection would make that operational contract less visible rather than cleaner.
Payload and Dependencies
// SendWelcomeEmailTypeName identifies the job during dispatch and handler registration.
const SendWelcomeEmailTypeName = "emails:welcome"
// SendWelcomeEmailPayload identifies the user whose current state the worker should load.
type SendWelcomeEmailPayload struct {
UserID string `json:"user_id"`
}
// SendWelcomeEmail dispatches and handles the welcome-email workflow.
type SendWelcomeEmail struct {
queues *queues.Manager
users *users.Service
}
// NewSendWelcomeEmail constructs the job with its queue and application dependencies.
func NewSendWelcomeEmail(queues *queues.Manager, users *users.Service) *SendWelcomeEmail {
return &SendWelcomeEmail{queues: queues, users: users}
}Job names should be stable operational identifiers.
Dispatch
Jobs own their dispatch shape. Add time to the file's imports when applying this policy:
// Queue dispatches the welcome-email job with its retry and timeout policy.
func (j *SendWelcomeEmail) Queue(ctx context.Context, userID string) error {
payload, err := json.Marshal(SendWelcomeEmailPayload{UserID: userID})
if err != nil {
return err
}
_, err = j.queues.WithContext(ctx).Dispatch(
queue.NewJob(SendWelcomeEmailTypeName).
Payload(payload).
OnQueue("emails").
Retry(3).
Backoff(2*time.Second).
Timeout(30*time.Second),
)
return err
}Services can call job.Queue(ctx, id) without constructing raw queue messages.
Retry(3) permits up to three application retry attempts after the first attempt. Backoff delays those retries, and Timeout bounds each attempt. Choose values from the side effect and service-level objective rather than copying these example values unchanged.
Handling
Handlers bind payloads and delegate business behavior:
// HandleTask loads the queued user reference and delegates delivery to the user service.
func (j *SendWelcomeEmail) HandleTask(ctx context.Context, msg queue.Message) error {
var payload SendWelcomeEmailPayload
if err := msg.Bind(&payload); err != nil {
return fmt.Errorf("bind send welcome email payload: %w", err)
}
return j.users.SendWelcomeEmail(ctx, payload.UserID)
}Return errors for retryable failures so the queue can apply the policy attached at dispatch. With errors imported, return queue.Permanent(err) for a terminal failure that should not spend the remaining application retry budget:
if errors.Is(err, users.ErrInvalidEmail) {
return queue.Permanent(err)
}
return errBroker redelivery after an infrastructure failure is distinct from the application retry budget. Review acknowledgement and durability behavior for the selected driver, and keep handlers idempotent even when Retry(0) is intentional.
Test Dispatch and Handling
Keep dispatch and handling as separate assertions.
For dispatch, inject a recording queue manager, call Queue, and assert the job type, logical queue, payload IDs, retry budget, backoff, and timeout. For handling, construct a message with the encoded payload and call the handler directly:
payload, err := json.Marshal(SendWelcomeEmailPayload{UserID: "user-123"})
if err != nil {
t.Fatal(err)
}
err = job.HandleTask(
context.Background(),
queue.NewMessage(SendWelcomeEmailTypeName, payload),
)Expected result: the fake user service receives user-123. Add cases for malformed payloads, retryable dependency errors, terminal errors, and repeated delivery. A direct handler test does not prove App registration, so keep one smoke test that dispatches through the generated manager and runs a worker.
In deployment, supervise ./bin/app worker or a queue-specific form such as ./bin/app worker --queue emails. After release, dispatch one safe job and confirm its App, queue, stable job name, and outcome in logs, metrics, and an Inspect when enabled.
Existing Job Registration
The generated-code tab shows the registration path for new jobs. The App-owned registerJobHandlers function is also the extension point for a manually written job.
Projects created before this registration seam may already contain custom job constructors that were never registered. Rerender migrates known framework jobs but does not guess whether an arbitrary provider is a job. Add each older custom job as a typed registerJobHandlers parameter and register its type name with queueManager.Register; future make:job calls maintain both entries automatically.
Do not register handlers after workers are already running.
Next Steps
- Queues explains queue configuration.
- Workers explains execution lifecycle.
- Retries and Idempotency explains safe retry behavior.
- Naming Conventions defines stable job names.
make:jobReference lists generation, queue selection, removal, and shared options.