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
- CustomerSendEmailJob - Sends emails to customers for subscription events
- MerchantSendEmailJob - Sends emails to merchants for subscription updates
- EnqueueInventoryFailureEmailJob - Schedules inventory failure emails across shops
- SendInventoryFailureEmailJob - Sends inventory failure notifications to merchants
CustomerSendEmailJob
Sends templated emails to customers for subscription contract events.
Parameters
Shop domain for the subscription
payload.admin_graphql_api_id
GraphQL Admin API ID of the subscription contract
Name of the email template to use (e.g., “subscription_created”, “billing_attempt_success”)
payload.admin_graphql_api_customer_id
GraphQL Admin API ID of the customer (optional, will be fetched if not provided)
Billing cycle index for cycle-specific emails (optional)
Configuration
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 domain to send merchant email for
payload.admin_graphql_api_id
GraphQL Admin API ID of the subscription contract
Configuration
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
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
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
- Validates frequency parameter against allowed values
- Retrieves all active billing schedules in batches
- Creates
SendInventoryFailureEmailJob for each shop
- Handles failures gracefully with
Promise.allSettled
- 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 domain to send inventory email for
Frequency trigger for this email batch (daily/weekly)
Configuration
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
- Loads shop settings from metaobject
- Checks if shop’s
inventoryNotificationFrequency matches job frequency
- Sends email only if frequencies match
- Skips gracefully if frequencies don’t match (no error)
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.