Creating Jobs
Quest jobs are Sails scripts that run on a schedule. They have full access to your models, helpers, and configurations.
Creating a New Job
Use the Sails CLI to generate a new script:
npx sails generate script cleanup-sessionsThis creates a new file at scripts/cleanup-sessions.js with the basic structure:
module.exports = {
friendlyName: 'Cleanup sessions',
description: '',
inputs: {},
fn: async function (inputs) {
// Your code here
}
}Then add the quest configuration to make it a scheduled job:
Basic Job Structure
Here's a complete Quest job:
// scripts/cleanup-sessions.js
module.exports = {
friendlyName: 'Cleanup old sessions',
description: 'Remove expired sessions from the database',
// Quest configuration
quest: {
interval: '1 hour'
// withoutOverlapping defaults to true
},
inputs: {
daysOld: {
type: 'number',
defaultsTo: 30
}
},
fn: async function (inputs) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - inputs.daysOld)
const deleted = await Session.destroy({
expiresAt: { '<': cutoffDate }
}).fetch()
sails.log.info(`Deleted ${deleted.length} expired sessions`)
return {
deletedCount: deleted.length,
cutoffDate
}
}
}Quest Configuration Block
The quest object in your script defines the scheduling:
quest: {
// Run every 30 minutes
interval: '30 minutes',
// Or use a cron expression
cron: '0 */2 * * *',
// Allow concurrent runs (default is prevented)
withoutOverlapping: false
}Using Models and Helpers
Jobs have full access to Sails features:
// scripts/send-weekly-newsletter.js
module.exports = {
friendlyName: 'Send weekly newsletter',
quest: {
cron: '0 9 * * MON' // Every Monday at 9 AM
},
fn: async function () {
// Use models
const subscribers = await User.find({
subscribed: true,
emailVerified: true
})
// Use helpers
for (const user of subscribers) {
await sails.helpers.sendEmail.with({
to: user.email,
template: 'newsletter',
subject: 'Weekly Newsletter',
templateData: {
userName: user.fullName
}
})
}
return { sent: subscribers.length }
}
}Job Inputs
Jobs can accept inputs with default values:
// scripts/process-uploads.js
module.exports = {
friendlyName: 'Process upload queue',
quest: {
interval: '5 minutes'
// withoutOverlapping defaults to true, preventing concurrent runs
},
inputs: {
batchSize: {
type: 'number',
defaultsTo: 10,
description: 'Number of uploads to process'
},
priority: {
type: 'string',
isIn: ['high', 'normal', 'low'],
defaultsTo: 'normal'
}
},
fn: async function (inputs) {
const uploads = await Upload.find({
status: 'pending',
priority: inputs.priority
})
.limit(inputs.batchSize)
.sort('createdAt ASC')
// Process uploads...
return { processed: uploads.length }
}
}Error Handling
Jobs should handle errors gracefully:
// scripts/sync-inventory.js
module.exports = {
friendlyName: 'Sync inventory with external API',
quest: {
interval: '30 minutes'
// Concurrent runs are prevented by default
},
fn: async function () {
try {
const response = await fetch(sails.config.custom.inventoryApiUrl, {
headers: {
Authorization: `Bearer ${sails.config.custom.apiKey}`
}
})
if (!response.ok) {
throw new Error(`API returned ${response.status}`)
}
const data = await response.json()
// Process data...
return { synced: data.length }
} catch (error) {
sails.log.error('Inventory sync failed:', error)
// Optionally send alert
await sails.helpers.sendAlert({
type: 'sync_failure',
error: error.message
})
throw error // Re-throw to trigger quest:job:error event
}
}
}Conditional Execution
Skip job execution based on conditions:
// scripts/backup-database.js
module.exports = {
friendlyName: 'Backup database',
quest: {
cron: '0 3 * * *' // Daily at 3 AM
},
fn: async function () {
// Check if backups are enabled
const settings = await Settings.findOne({ key: 'backups_enabled' })
if (!settings?.value) {
sails.log.info('Backups disabled, skipping...')
return { skipped: true }
}
// Check if it's the weekend (skip on weekends)
const day = new Date().getDay()
if (day === 0 || day === 6) {
return { skipped: true, reason: 'weekend' }
}
// Proceed with backup
const result = await sails.helpers.performBackup()
return result
}
}Long-Running Jobs
For jobs that take a long time:
// scripts/generate-reports.js
module.exports = {
friendlyName: 'Generate monthly reports',
quest: {
cron: '0 2 1 * *' // First day of month at 2 AM
// withoutOverlapping defaults to true - essential for long-running jobs
},
fn: async function () {
const startTime = Date.now()
// Process in batches to avoid memory issues
const BATCH_SIZE = 100
let processed = 0
let hasMore = true
while (hasMore) {
const users = await User.find().skip(processed).limit(BATCH_SIZE)
if (users.length === 0) {
hasMore = false
break
}
for (const user of users) {
await generateUserReport(user)
}
processed += users.length
sails.log.info(`Processed ${processed} users...`)
}
return {
processed,
duration: Date.now() - startTime
}
}
}Running Scripts Without Scheduling
Quest can run ANY Sails script programmatically, even without quest metadata:
// scripts/one-time-task.js - No quest config needed!
module.exports = {
friendlyName: 'One-time task',
inputs: {
userId: {
type: 'number',
required: true
}
},
fn: async function ({ userId }) {
const user = await User.findOne({ id: userId })
// Do something with user...
return { processed: user.email }
}
}
// Run it programmatically from anywhere in your app:
await sails.quest.run('one-time-task', { userId: 123 })This is useful for:
- Running scripts from controllers or helpers
- Triggering tasks based on user actions
- Testing scripts before adding scheduling
- One-time administrative tasks
Best Practices
Generate scripts with CLI - Use
sails generate script <name>for consistencyUse descriptive names - Name your job files clearly (e.g.,
cleanup-old-sessions.js)Add friendly names and descriptions - Help future developers understand the job's purpose
Use withoutOverlapping - Prevent concurrent runs for jobs that shouldn't overlap
Return meaningful data - Return statistics or results for monitoring
Handle errors gracefully - Log errors and optionally send alerts
Keep jobs focused - Each job should do one thing well
Consider performance - Use pagination for large datasets
Test locally - Run jobs manually with
sails.quest.run('job-name')before deploying