Tasks
The Client and its Enqueue method are enough to submit a raw *asynq.Task, but every layer built on queue, from the events module's listener fan-out to the scheduler's cron entries, shares three more things: a common set of enqueue options, a common envelope format for event payloads, and a common task-type naming scheme. This page covers all three; day to day you'll meet them again, unchanged, wherever queue is used underneath.
Enqueue options
queue re-exports the useful subset of asynq's options so common cases don't require importing asynq:
| Option | Effect |
|---|---|
OnQueue(name) | routes to a named queue (matched against the worker's WORKER_QUEUES weights); unset means default |
MaxRetry(n) | how many retries before a failing task is archived |
Timeout(d) | bounds a single execution attempt |
Deadline(t) | absolute deadline across all attempts |
ProcessIn(d) | delays processing by d |
Unique(ttl) | suppresses an identical task (same type + payload) while a previous one is pending |
Retention(d) | keeps the completed task in Valkey for d, for inspection |
TaskID(id) | explicit task ID; a conflict surfaces as ErrDuplicate (the basis of idempotent delivery) |
The options look the same everywhere a task is born: at Enqueue, at the events.Listen listener registration, and on scheduler entries. The shop's mail queue, for example, appears in the listener registration:
events.Listen(reg, "sendOrderConfirmation", sendOrderConfirmation(deps), queue.OnQueue("mail"))
When you need the full asynq option set, queue.AsynqOptions(opts...) converts these into raw []asynq.Option, and asynq's own API remains open alongside Enqueue: an escape hatch, not a forbidden zone.
The Envelope
Every event task payload is the same wire format, queue.Envelope:
type Envelope struct {
ID string `json:"id"` // dispatch id, doubles as the asynq TaskID
Name string `json:"name"` // the event name (EventName())
Payload json.RawMessage `json:"payload"` // the JSON-encoded event value
Metadata map[string]string `json:"metadata,omitempty"` // W3C trace context
OccurredAt time.Time `json:"occurred_at"`
}
The envelope travels unchanged from the dispatcher (or the outbox) through the fan-out task to every listener task: the listener sees the same ID, payload and trace context the producer set. env.Task() renders it as the fan-out task; queue.DecodeEnvelope(task) unpacks it on the other side.
Trace propagation across the queue
env.InjectTrace(ctx) writes the context's trace context into Metadata in W3C form (traceparent/tracestate); on the worker side, env.ExtractTrace(ctx) continues the same trace. That makes the HTTP request → outbox → relay → worker → listener path a single trace, in the shop from placing the order to the confirmation e-mail. The dispatchers and the worker middleware do this automatically; see the OpenTelemetry page for details.
Task types
Four task-type prefixes cover every task that flows through queue in the async category, and the helpers below are the single source of the wire names:
| Helper | Task type | Created by |
|---|---|---|
EventTaskType(name) | event:shop.orderPlaced | dispatch / outbox relay (the starting point of the fan-out) |
ListenerTaskType(event, listener) | listener:shop.orderPlaced:sendOrderConfirmation | the worker's fan-out handler, one per listener |
ScheduleTaskType(name) | schedule:shop.someEvent | the scheduler, when firing a scheduled event |
JobTaskType(name) | job:shop.nightlySalesReport | the scheduler, when firing a scheduled job |
The kit's worker routes tasks to its four built-in handlers by these prefixes; the IsEventTaskType/IsListenerTaskType/IsJobTaskType/IsScheduleTaskType and JobName helpers give the same classification to anyone working with raw tasks (say, in a handler registered via WithMux). These are also the names you see in Asynqmon and in the worker's logs: you can tell at a glance which task is which listener of which event. The events module and the worker chassis are what actually produce and consume tasks under these names; this page only defines the naming, not the fan-out itself.
When to reach for it directly
- Your own, non-event task: build an
asynq.NewTask(...), submit it withclient.Enqueue, and register a handler for it in the worker withWithMux. - An idempotent "act exactly once" enqueue:
TaskID+ErrDuplicatehandling, as on the Overview page. - Everything else (event dispatch, scheduling) belongs to the higher layers: start at events.