Skip to main content

Overview

Email jobs handle automated email notifications for both customers and merchants in the subscription lifecycle. These jobs manage subscription event notifications, inventory alerts, and use configurable email templates.

Job Types

  1. CustomerSendEmailJob - Sends emails to customers for subscription events
  2. MerchantSendEmailJob - Sends emails to merchants for subscription updates
  3. EnqueueInventoryFailureEmailJob - Schedules inventory failure emails across shops
  4. SendInventoryFailureEmailJob - Sends inventory failure notifications to merchants

CustomerSendEmailJob

Sends templated emails to customers for subscription contract events.

Parameters

shop
string
required
Shop domain for the subscription
payload.admin_graphql_api_id
string
required
GraphQL Admin API ID of the subscription contract
payload.emailTemplate
string
required
Name of the email template to use (e.g., “subscription_created”, “billing_attempt_success”)
payload.admin_graphql_api_customer_id
string
GraphQL Admin API ID of the customer (optional, will be fetched if not provided)
payload.cycle_index
number
Billing cycle index for cycle-specific emails (optional)

Configuration

queue
string
default:"webhooks"
Assigned to the webhooks queue for webhook-triggered emails

Implementation

export class CustomerSendEmailJob extends Job<
  Jobs.Parameters<Webhooks.SubscriptionContractEvent>
> {
  public queue: string = 'webhooks';

  async perform(): Promise<void> {
    const {shop, payload} = this.parameters;

    let {
      admin_graphql_api_id: subscriptionContractId,
      emailTemplate: subscriptionTemplateName,
      admin_graphql_api_customer_id: customerId,
      cycle_index: billingCycleIndex,
    } = payload;

    if (!customerId) {
      customerId = await getContractCustomerId(shop, subscriptionContractId);
    }

    const templateInput: CustomerEmailTemplateInput = {
      subscriptionContractId,
      subscriptionTemplateName,
    };

    if (billingCycleIndex) {
      templateInput.billingCycleIndex = billingCycleIndex;
    }
    
    await new CustomerSendEmailService().run(shop, customerId, templateInput);
  }
}
Source: app/jobs/email/CustomerSendEmailJob.ts:8-37

Email Templates

Common customer email template names:
  • subscription_created - Welcome email when subscription is created
  • subscription_updated - Contract details changed
  • billing_attempt_success - Payment processed successfully
  • billing_attempt_failure - Payment failed notification
  • subscription_paused - Subscription paused by merchant or system
  • subscription_cancelled - Subscription cancelled
  • upcoming_billing_cycle - Reminder before next billing

Template Data

The CustomerSendEmailService populates templates with:
  • Subscription contract details
  • Customer information
  • Billing cycle data (if billingCycleIndex provided)
  • Product/variant information
  • Next billing date

Webhook Trigger

import {CustomerSendEmailJob} from '~/jobs/email';
import {jobs} from '~/jobs';

export async function handleSubscriptionCreated(
  shop: string,
  payload: Webhooks.SubscriptionContractCreated,
) {
  const emailPayload = {
    ...payload,
    emailTemplate: 'subscription_created',
  };
  
  const job = new CustomerSendEmailJob({shop, payload: emailPayload});
  await jobs.enqueue(job);
}
If customerId is not provided in the payload, the job automatically fetches it from the subscription contract to ensure email delivery.

MerchantSendEmailJob

Sends notification emails to shop merchants about subscription events.

Parameters

shop
string
required
Shop domain to send merchant email for
payload.admin_graphql_api_id
string
required
GraphQL Admin API ID of the subscription contract

Configuration

queue
string
default:"webhooks"
Assigned to the webhooks queue

Implementation

export class MerchantSendEmailJob extends Job<
  Jobs.Parameters<Webhooks.SubscriptionContractId>
> {
  public queue: string = 'webhooks';

  async perform(): Promise<void> {
    const {shop, payload} = this.parameters;
    const {admin_graphql_api_id: subscriptionContractId} = payload;

    const merchantTemplateInput = {
      subscriptionContractId,
      subscriptionTemplateName:
        MerchantEmailTemplateName.SubscriptionCancelledMerchant,
    };

    await new MerchantSendEmailService().run(shop, merchantTemplateInput);
  }
}
Source: app/jobs/email/MerchantSendEmailJob.ts:8-25

Merchant Email Templates

Currently implements SubscriptionCancelledMerchant template. The template name is defined in:
enum MerchantEmailTemplateName {
  SubscriptionCancelledMerchant = 'subscription_cancelled_merchant',
}

Webhook Trigger

import {MerchantSendEmailJob} from '~/jobs/email';
import {jobs} from '~/jobs';

export async function handleSubscriptionCancelled(
  shop: string,
  payload: Webhooks.SubscriptionContractCancelled,
) {
  const job = new MerchantSendEmailJob({shop, payload});
  await jobs.enqueue(job);
}
Merchant emails are sent to the shop owner’s email address configured in Shopify. Ensure shop contact information is up to date.

EnqueueInventoryFailureEmailJob

Schedules inventory failure notification emails for all active shops based on configured frequency.

Parameters

frequency
enum
required
Email frequency setting to match against shop preferences:
  • daily - Send daily inventory reports
  • weekly - Send weekly inventory reports
  • never - Don’t send emails (no jobs created)

Configuration

queue
string
default:"default"
Uses default queue for coordination job

Implementation

export class EnqueueInventoryFailureEmailJob extends Job<
  Jobs.SendInventoryFailureEmailParameters
> {
  async perform(): Promise<void> {
    if (!IsValidInventoryNotificationFrequency(this.parameters.frequency)) {
      this.logger.error(
        {frequency: this.parameters.frequency},
        'Invalid frequency',
      );
      return;
    }

    await findActiveBillingSchedulesInBatches(async (batch) => {
      this.logger.info(
        `Scheduling SendInventoryFailureEmailJob for ${batch.length} shops`,
      );

      const results = await Promise.allSettled(
        batch.map(async (billingSchedule) => {
          const params: Jobs.Parameters<Jobs.SendInventoryFailureEmailParameters> =
            {
              shop: billingSchedule.shop,
              payload: {
                frequency: this.parameters.frequency,
              },
            };

          const job = new SendInventoryFailureEmailJob(params);

          try {
            await jobs.enqueue(job);
            return true;
          } catch (err) {
            const error = err instanceof Error ? err : new Error(String(err));
            this.logger.error(error);
            return false;
          }
        }),
      );

      const startedJobsCount = results.filter(
        (result) => isFulfilled(result) && result.value,
      ).length;

      this.logger.info(
        {successCount: startedJobsCount, batchCount: results.length},
        'Successfully enqueued jobs',
      );
    });
  }
}
Source: app/jobs/email/EnqueueInventoryFailureEmailJob.ts:9-63

Behavior

  1. Validates frequency parameter against allowed values
  2. Retrieves all active billing schedules in batches
  3. Creates SendInventoryFailureEmailJob for each shop
  4. Handles failures gracefully with Promise.allSettled
  5. Logs success/failure counts for monitoring

Scheduling

Typically triggered by external scheduler: Daily (runs at midnight):
const job = new EnqueueInventoryFailureEmailJob({frequency: 'daily'});
await jobs.enqueue(job);
Weekly (runs on Mondays):
const job = new EnqueueInventoryFailureEmailJob({frequency: 'weekly'});
await jobs.enqueue(job);
This job fans out to create individual jobs per shop, allowing for parallel processing and isolated failure handling.

SendInventoryFailureEmailJob

Sends inventory failure summary email to a specific merchant if their notification preferences match.

Parameters

shop
string
required
Shop domain to send inventory email for
payload.frequency
enum
required
Frequency trigger for this email batch (daily/weekly)

Configuration

queue
string
default:"default"
Uses default queue

Implementation

function isEmailable(frequency: string, frequencySettings: string): boolean {
  return (
    IsValidInventoryNotificationFrequency(frequencySettings) &&
    frequencySettings === frequency
  );
}

export class SendInventoryFailureEmailJob extends Job<
  Jobs.Parameters<Jobs.SendInventoryFailureEmailParameters>
> {
  public queue: string = 'default';

  async perform(): Promise<void> {
    const {shop} = this.parameters;
    const frequency = this.parameters.payload.frequency;

    const {admin} = await unauthenticated.admin(shop);
    const settings = await loadSettingsMetaobject(admin.graphql);

    if (!settings) {
      this.logger.error(
        {
          shopDomain: shop,
          frequency: frequency,
        },
        'Failed to load settings from metaobject for shop',
      );
      return;
    }

    if (isEmailable(frequency, settings.inventoryNotificationFrequency)) {
      this.logger.info(
        {
          shopDomain: shop,
          frequency: frequency,
        },
        'Sending email with MerchantSendSubscriptionInventoryEmailService',
      );

      await new MerchantSendSubscriptionInventoryEmailService().run(shop);
    } else {
      this.logger.info(
        {
          shopDomain: shop,
          frequency: frequency,
        },
        'Skipping inventory failure email because job frequency does not match shop setting',
      );
    }
  }
}
Source: app/jobs/email/SendInventoryFailureEmailJob.ts:8-58

Behavior

  1. Loads shop settings from metaobject
  2. Checks if shop’s inventoryNotificationFrequency matches job frequency
  3. Sends email only if frequencies match
  4. Skips gracefully if frequencies don’t match (no error)

Settings Metaobject

The job expects shop settings stored as a metaobject:
{
  "inventoryNotificationFrequency": "daily" | "weekly" | "never"
}

Email Service

MerchantSendSubscriptionInventoryEmailService generates an email containing:
  • List of subscription contracts with inventory failures
  • Product details and quantities needed
  • Links to manage inventory in Shopify admin
  • Summary of affected subscriptions
If shop settings cannot be loaded, the job logs an error and returns early without sending email. This prevents spamming merchants with unwanted notifications.

Email Service Architecture

Email jobs delegate to service classes for template rendering and delivery:

CustomerSendEmailService

import {CustomerSendEmailService} from '~/services/CustomerSendEmailService';

const service = new CustomerSendEmailService();
await service.run(shop, customerId, {
  subscriptionContractId,
  subscriptionTemplateName: 'billing_attempt_success',
  billingCycleIndex: 5,
});

MerchantSendEmailService

import {MerchantSendEmailService} from '~/services/MerchantSendEmailService';

const service = new MerchantSendEmailService();
await service.run(shop, {
  subscriptionContractId,
  subscriptionTemplateName: 'subscription_cancelled_merchant',
});

MerchantSendSubscriptionInventoryEmailService

import {MerchantSendSubscriptionInventoryEmailService} from '~/services/MerchantSendSubscriptionInventoryEmailService';

const service = new MerchantSendSubscriptionInventoryEmailService();
await service.run(shop);

Job Registration

Email jobs are registered in app/jobs/index.ts:
export const jobs = (() => {
  // ... scheduler setup
})().register(
  CustomerSendEmailJob,
  MerchantSendEmailJob,
  EnqueueInventoryFailureEmailJob,
  SendInventoryFailureEmailJob,
  // ... other jobs
);
Source: app/jobs/index.ts:72-91

Testing Email Jobs

Unit Testing

import {CustomerSendEmailJob} from '~/jobs/email';

const job = new CustomerSendEmailJob({
  shop: 'test-shop.myshopify.com',
  payload: {
    admin_graphql_api_id: 'gid://shopify/SubscriptionContract/1',
    emailTemplate: 'subscription_created',
    admin_graphql_api_customer_id: 'gid://shopify/Customer/1',
  },
});

await job.perform();

Mock Email Service

import {CustomerSendEmailService} from '~/services/CustomerSendEmailService';

jest.spyOn(CustomerSendEmailService.prototype, 'run')
  .mockResolvedValue(undefined);

// Run job tests without sending actual emails
Email services should implement logging for all sent emails to support debugging and compliance requirements.

Build docs developers (and LLMs) love