Skip to content

Creating Jobs

Jobs are the core building blocks of your queue system. They encapsulate the work that needs to be done in the background.

Generate a Job

Use the Ace command to create a new job:

node ace make:job send_email

This creates a job file at app/jobs/send_email_job.ts:

import { Job } from '@nemoventures/adonis-jobs'
 
/**
 * Here you can define the data type your job expects.
 */
export type SendEmailJobData = {
  to: string
  subject: string
  template: string
  variables: Record<string, any>
}
 
/**
 * Then you can also define the return type of your job.
 */
export type SendEmailJobReturn = {
  messageId: string
  success: boolean
}
 
export default class SendEmailJob extends Job<SendEmailJobData, SendEmailJobReturn> {
  async process(): Promise<SendEmailJobReturn> {
    const { to, subject, template, variables } = this.data
 
    // Send email logic here
    const messageId = await sendEmail({ to, subject, template, variables })
 
    return { messageId, success: true }
  }
}

Dependency Injection

You can inject dependencies with the @inject() decorator in your job classes and methods like you would expect in AdonisJS.

import { inject } from '@adonisjs/core'
import { Job } from '@nemoventures/adonis-jobs'
import MailService from '#services/mail_service'
 
export type SendEmailJobData = {
  to: string
  subject: string
  body: string
}
 
@inject()
export default class SendEmailJob extends Job<SendEmailJobData, void> {
  constructor(private mailService: MailService) {
    super()
  }
 
  @inject()
  async process(anotherService: AnotherService): Promise<void> {
    await this.mailService.send(this.data)
    await anotherService.doSomething(this.data)
  }
}

Job Properties

Your job has access to several properties:

export default class SendEmailJob extends Job<SendEmailJobData, SendEmailJobReturn> {
  async process(): Promise<SendEmailJobReturn> {
    // Job data passed during dispatch
    const emailData = this.data
 
    // BullMQ Job instance
    const jobId = this.job
 
    // BullMQ Worker instance
    const worker = this.worker
 
    // Logger instance
    this.logger.info('Processing email job', { to: emailData.to })
 
    return { messageId: 'abc123', success: true }
  }
}

Default Queue

Set a default queue for a job class:

import { Queues } from '@nemoventures/adonis-jobs/types'
 
export default class SendEmailJob extends Job<SendEmailJobData, void> {
  static defaultQueue: keyof Queues = 'emails'
 
  async process(): Promise<void> {
    // Job logic
  }
}

Now all instances of this job will use the emails queue unless overridden during dispatch.

Default Options

Set default BullMQ options for a job class to avoid repeating them every time you dispatch:

import { Job } from '@nemoventures/adonis-jobs'
import type { BullJobsOptions } from '@nemoventures/adonis-jobs/types'
 
export default class SendEmailJob extends Job<void, void> {
  static options: BullJobsOptions = {
    attempts: 5,
    backoff: {
      type: 'exponential',
      delay: 1000,
    },
    removeOnComplete: 10,
    removeOnFail: 50,
  }
 
  async process(): Promise<void> {
    // Job logic
  }
}

Now you can dispatch the job without having to specify options every time:

// These options will be automatically applied
await SendEmailJob.dispatch(data)
 
// You can still override specific options if needed when dispatching
await SendEmailJob.dispatch(data)
  .with('attempts', 3)
  .with('delay', 5000)

The .with() method will merge with and override the default options.

Error Handling

Failing Jobs

Explicitly fail a job to skip retries:

export default class ProcessPaymentJob extends Job<PaymentData, void> {
  async process(): Promise<void> {
    if (!this.data.paymentMethodId) {
      this.fail('Payment method ID is required')
      return
    }
 
    // Process payment
  }
}

Calling this.fail() will throw an UnrecoverableError. In this case, BullMQ will move the job to the failed state and will not retry it.

See the BullMQ documentation for more details.

Handling Failures

Override the onFailed method to handle job failures:

export default class SendEmailJob extends Job<SendEmailJobData, void> {
  async process(): Promise<void> {
    // Email sending logic that might fail
  }
 
  async onFailed(): Promise<void> {
    this.logger.error({ err: this.error }, 'Email job failed')
 
    // Check if all attempts have been exhausted
    if (this.allAttemptsMade()) {
      await this.notifyAdministrators()
    }
  }
}

Job Discovery

Jobs can be placed anywhere in your codebase. The only requirement is that they:

  • End with _job.ts suffix
  • Export a default class extending Job
✅ app/jobs/send_email_job.ts
✅ app/jobs/process_payment_job.ts
✅ app/jobs/generate_report_job.ts
 
❌ app/jobs/send_email.ts
❌ app/jobs/email_sender.ts

The package will automatically discover all job files in your codebase, so you don't need to register them manually. What will happen in the background when dispatching a job is that the package will store the job class name in Redis, and when processing the job, it will dynamically import the job file based on the class name.

One caveat of this approach is that if you rename a Job class name (not the file path), and you have already dispatched jobs in the queue, those jobs will fail to process because the package will not be able to find the job class.

To avoid this, you can set the nameOverride property in the job class:

/**
 * Let's say this class was previously named `SendEmailJob` and you
 * renamed it to `SendEmailNotificationJob`.
 */
export default class SendEmailNotificationJob extends Job<SendEmailJobData, void> {
  // This will allow the previously dispatched jobs to be processed correctly
  static nameOverride = 'SendEmailJob'
 
  async process(): Promise<void> {
    // Job logic
  }
}

Next Steps

Now that you know how to create jobs, learn how to dispatch them to your queues.