Quest
Quest is a job scheduler dashboard for your sails-hook-quest powered applications. View scheduled jobs, trigger manual runs, and control job execution—all from the Slipway dashboard.
What is Quest?
Quest provides a web-based interface for managing your scheduled jobs:
- View all jobs - See every scheduled job with its schedule and status
- Run jobs manually - Trigger immediate execution of any job
- Pause/Resume - Control which jobs are running
- Monitor status - See if jobs are currently executing
No SSH access needed—manage your background jobs from the dashboard.
Requirements
Quest is available when your app uses sails-hook-quest:
npm install sails-hook-questSlipway automatically detects sails-hook-quest during deployment and enables the Quest feature.
Accessing Quest
Via Dashboard
- Go to your project in Slipway
- Select an environment and click the app name from the Apps list
- Click the ellipsis dropdown menu and select Quest
- View and manage your jobs
Via Direct URL
https://your-slipway-instance.com/projects/myapp/questOr with a specific environment:
https://your-slipway-instance.com/projects/myapp/environments/staging/questJob Dashboard
The Quest dashboard shows all scheduled jobs:
┌─────────────────────────────────────────────────────────────────┐
│ Quest sails-hook-quest │
├─────────────────────────────────────────────────────────────────┤
│ │
│ cleanup-sessions Active no overlap │
│ Remove expired sessions from the database │
│ ⏱ every 1 hour │
│ [Run now] [Pause] │
│ │
│ send-newsletter Paused │
│ Send weekly newsletter to subscribers │
│ ⏱ cron: 0 9 * * MON │
│ [Run now] [Resume] │
│ │
│ process-uploads Running │
│ Process pending file uploads │
│ ⏱ every 2 minutes │
│ [Running...] │
│ │
└─────────────────────────────────────────────────────────────────┘Job Information
Each job displays:
| Field | Description |
|---|---|
| Name | The job's friendly name or script name |
| Description | What the job does |
| Schedule | Cron expression or interval |
| Status | Active, Paused, or Running |
| No overlap | Badge shown if concurrent runs are prevented |
Job Status
| Status | Description |
|---|---|
| Active | Job is scheduled and will run at its next scheduled time |
| Paused | Job won't run until resumed |
| Running | Job is currently executing |
Actions
Run Now
Trigger immediate execution of a job:
- Click Run now on any job
- The job starts executing in your app
- Status updates to "Running" while executing
TIP
Running a job manually doesn't affect its regular schedule. The job will still run at its next scheduled time.
Pause
Stop a job from running on schedule:
- Click Pause on an active job
- Status changes to "Paused"
- Job won't run until resumed
Pausing is useful for:
- Temporarily stopping resource-intensive jobs
- Debugging job-related issues
- Maintenance windows
Resume
Re-enable a paused job:
- Click Resume on a paused job
- Status changes to "Active"
- Job resumes normal scheduling
Creating Jobs
Jobs are defined in your Sails app's scripts/ directory with a quest property:
// scripts/cleanup-sessions.js
module.exports = {
friendlyName: 'Cleanup old sessions',
description: 'Remove expired sessions from the database',
quest: {
interval: '1 hour',
withoutOverlapping: true
},
fn: async function () {
const deleted = await Session.destroy({
lastActive: {
'<': new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
}
}).fetch()
sails.log.info(`Cleaned up ${deleted.length} sessions`)
return { deletedCount: deleted.length }
}
}Schedule Options
Intervals (Human-Readable)
quest: {
interval: '30 seconds'
interval: '5 minutes'
interval: '2 hours'
interval: '7 days'
}Cron Expressions
quest: {
cron: '0 2 * * *' // Daily at 2 AM
cron: '*/5 * * * *' // Every 5 minutes
cron: '0 9 * * MON' // Every Monday at 9 AM
}One-Time Execution
quest: {
timeout: '10 minutes' // Run once after 10 minutes
}Overlap Prevention
Prevent concurrent runs of the same job:
quest: {
interval: '5 minutes',
withoutOverlapping: true // Skip if already running
}API Endpoints
Quest provides REST API endpoints for programmatic control:
List Jobs
GET /api/v1/projects/:projectSlug/quest/jobsResponse:
{
"jobs": [
{
"name": "cleanup-sessions",
"friendlyName": "Cleanup old sessions",
"description": "Remove expired sessions",
"schedule": "1 hour",
"scheduleType": "interval",
"paused": false,
"withoutOverlapping": true,
"isRunning": false
}
]
}Run a Job
POST /api/v1/projects/:projectSlug/quest/jobs/:name/runOptional body:
{
"inputs": {
"daysOld": 7
}
}Pause a Job
POST /api/v1/projects/:projectSlug/quest/jobs/:name/pauseResume a Job
POST /api/v1/projects/:projectSlug/quest/jobs/:name/resumeJob Examples
Database Cleanup
// scripts/cleanup-old-data.js
module.exports = {
friendlyName: 'Cleanup old data',
description: 'Remove records older than 90 days',
quest: {
cron: '0 3 * * *', // Daily at 3 AM
withoutOverlapping: true
},
fn: async function () {
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
await AuditLog.destroy({ createdAt: { '<': cutoff } })
await TempFile.destroy({ createdAt: { '<': cutoff } })
sails.log.info('Old data cleanup complete')
}
}Email Digest
// scripts/send-daily-digest.js
module.exports = {
friendlyName: 'Send daily digest',
description: 'Send summary email to active users',
quest: {
cron: '0 8 * * *', // Daily at 8 AM
withoutOverlapping: true
},
fn: async function () {
const users = await User.find({
digestEnabled: true,
emailVerified: true
})
for (const user of users) {
await sails.helpers.mail.sendDigest(user)
}
return { sent: users.length }
}
}Queue Processing
// scripts/process-queue.js
module.exports = {
friendlyName: 'Process job queue',
description: 'Process pending background jobs',
quest: {
interval: '30 seconds',
withoutOverlapping: true
},
fn: async function () {
const pending = await Job.find({
status: 'pending'
}).limit(10)
for (const job of pending) {
try {
await sails.helpers.jobs.process(job)
await Job.updateOne({ id: job.id }).set({ status: 'completed' })
} catch (err) {
await Job.updateOne({ id: job.id }).set({
status: 'failed',
error: err.message
})
}
}
return { processed: pending.length }
}
}Best Practices
1. Always Use withoutOverlapping
For jobs that shouldn't run concurrently:
quest: {
interval: '5 minutes',
withoutOverlapping: true
}2. Keep Jobs Idempotent
Jobs should be safe to run multiple times:
// Good - checks before acting
const unprocessed = await Order.find({ processed: false })
for (const order of unprocessed) {
await processOrder(order)
await Order.updateOne({ id: order.id }).set({ processed: true })
}
// Bad - might double-process
const orders = await Order.find()
for (const order of orders) {
await processOrder(order) // Might run twice!
}3. Add Logging
Log job progress for debugging:
fn: async function () {
sails.log.info('Starting cleanup job')
const count = await Record.destroy({ old: true }).fetch()
sails.log.info(`Cleanup complete: ${count.length} records removed`)
return { removed: count.length }
}4. Handle Errors Gracefully
Jobs should catch and log errors:
fn: async function () {
try {
await riskyOperation()
} catch (err) {
sails.log.error('Job failed:', err)
// Optionally notify admins
await sails.helpers.mail.sendAlert({
subject: 'Job failed: cleanup',
error: err.message
})
throw err // Re-throw to mark job as failed
}
}Troubleshooting
Jobs Not Appearing
If jobs don't show in the dashboard:
- Verify
sails-hook-questis inpackage.json - Deploy your app (detection happens during deployment)
- Ensure the app is running
- Check that scripts have a
questproperty
Jobs Not Running
If scheduled jobs aren't executing:
- Check if the job is paused
- Verify the schedule syntax
- Check container logs for errors
- Ensure
autoStart: truein quest config
Manual Run Fails
If "Run now" fails:
- Check the app is running
- Look at container logs for errors
- Verify the script exists and has no syntax errors
What's Next?
- Learn about sails-hook-quest for setting up jobs
- Use Helm for debugging
- Set up Auto-Deploy for continuous deployment