Documentation previewThese docs are actively being built. Some pages may change as the framework and examples are finalized.
Skip to content

Jobs

A Job is a named unit of queued work with a payload and a registered handler.

Jobs make background work explicit, observable, retryable, and operable.

When To Use Jobs

QuestionGuidance
Use this whenBackground work needs a stable name, typed payload, handler, retry behavior, or worker lifecycle.
Avoid this whenThe behavior is just a local function call or a typed fact that subscribers may observe.
Start withA small payload containing IDs and references to source-of-truth data.
Upgrade toDedicated queues, retry policy, idempotency keys, and worker process planning as operational risk grows.

Generated Package

Job code usually lives in:

text
internal/jobs

Create a job:

bash
forj make:job SendWelcomeEmail

Stamp a generated dispatch helper with a named queue when the job belongs to a specific operational lane:

bash
forj make:job reports:generate --queue reports

For a named app, run the make command through that app:

bash
forj marketplace make:job sync-catalog --queue sync

The app prefix routes the generated provider into the selected app's Wire files. In this example, GoForj updates app/marketplace/wire/inject_jobs_app.go instead of the default app's app/wire/inject_jobs_app.go.

Use category:action for job names, such as emails:send or reports:generate. See Naming Conventions for the full naming map.

Job Shape

go
const SendWelcomeEmailTypeName = "emails:welcome"

type SendWelcomeEmailPayload struct {
	UserID string `json:"user_id"`
}

type SendWelcomeEmail struct {
	queues *queues.Manager
	users  *users.Service
}

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:

go
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"),
	)
	return err
}

Services can call job.Queue(ctx, id) without constructing raw queue messages.

Handling

Handlers bind payloads and delegate business behavior:

go
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 when the job should fail and let queue behavior handle retry policy.

Registration

Generated App construction registers job handlers before workers start. forj make:job adds both the constructor and its handler registration to app/wire/inject_jobs_app.go, or app/<name>/wire/inject_jobs_app.go for a named App. The App-owned registerJobHandlers function is 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.

Common Mistakes

Common mistakes

  • Do not hide job names behind anonymous functions.
  • Do not put all business logic in HandleTask; delegate to services.
  • Do not use untyped map[string]any payloads when a typed payload is clearer.
  • Do not swallow handler errors that should be retried or observed.
  • Do not dispatch jobs from repositories unless persistence code intentionally owns that side effect.

Next Steps