Scripts let you automate repetitive workflows in your moving company. When a new lead comes in, a script can trigger Ask Bee to call the customer, send an SMS, and queue follow-ups for the next 5 days. When an estimate is signed, a script can add valuation charges, send a booking confirmation, and assign the crew.
This guide covers everything you need to write, test, and deploy scripts.
Quick Start
The fastest way to get started:
- Go to Settings → Scripts in your zone
- Click Add Script
- Choose Script URL and select a sample like “Web Lead Followup”
- Click Create — it auto-runs the install hook to set up tags
How Scripts Work
┌─────────────────────────────────────────────────────────────────────┐
│ Trigger Event │
│ (job-created, job-stage-changed, tag-added, custom, etc.) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Script Matching Query │
│ Find enabled scripts with matching hook in same or ancestor zone │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ QuickJS Sandbox Execution │
│ 1. Pre-fetch zone branding (__branding global) │
│ 2. Inject host functions (sendSMS, addComment, etc.) │
│ 3. Evaluate script → runs hook registrations │
│ 4. Call registered hook callback with context │
└─────────────────────────────────────────────────────────────────────┘
Scripts register listeners for triggers using hook(triggerName, callback). When the trigger fires, the callback runs with a context object containing job data, users, events, and more.
Script Anatomy
/// <reference path="./script.d.ts" />
/** @jsx h */
/** @jsxFrag Fragment */
/**
* @name My Script Name
* @description What this script does.
* @hooks job-created, custom-hook-1, install
*/
hook('job-created', (ctx) => {
const job = ctx.data.job;
if (!job) return;
sendSMS({ body: 'Welcome!' });
addTag(ctx, 'New Lead');
});
hook('install', async (ctx) => {
log('Setting up...');
await createZoneTag('New Lead');
});
Metadata Comments
Extracted automatically and displayed in the UI:
@name— Script display name@description— Brief description@hooks— List of hooks (used for filtering)
The install Hook
Scripts with an install hook auto-run it on creation. Use it to:
- Create required tags
- Log setup instructions
- Check zone configuration
hook('install', async (ctx) => {
log('Setting up...');
await Promise.all([
createZoneTag('New Lead'),
createZoneTag('Lead Day:Day 1'), // GitLab-style scoped label
]);
log('Setup complete!');
});
Available Triggers
| Trigger | When it fires |
|---|---|
job-created | New job created |
job-stage-changed | Job moved to new stage (lead → estimate → booking → invoice) |
job-status-changed | Job opened or closed |
tag-added | A tag was added to the job |
tag-removed | A tag was removed from the job |
email-received | Customer replied via email |
sms-received | Customer replied via SMS |
estimate-signed | Customer signed an estimate |
email-link-clicked | Customer clicked a link in an email |
job-event-created | New event added to job |
comment-created | New comment on job |
Custom Triggers
Use schedule() to create custom triggers:
schedule('my-followup', '1 hour', { attempt: 1 }, ctx);
hook('my-followup', (ctx) => {
// Runs 1 hour later
});
Host Functions Reference
Communication
sendSMS(input: { body: string; to?: string })
Sends an SMS to the job’s customer. Omit to to send to all customer phones.
sendSMS({
body: `Hi ${name}, thanks for reaching out!`,
});
sendEmail(input: { subject: string; html?: string; text?: string; markdown?: string })
Sends an email to the job’s customer.
sendEmail({
subject: 'Welcome!',
html: '<p>Thanks for choosing us!</p>',
});
addCommentToJob(input: { text?: string; mode?: string; attributes?: string[] })
Adds a comment to the job activity feed.
Common modes:
'System'— Internal note (staff only)'Ask Bee'— Triggers AI assistant (requires'AI_RESPONSE_REQUESTED'in attributes)'Send SMS to Customer'— Outbound SMS'Send Email to Customer'— Outbound email
// Trigger Ask Bee
addCommentToJob({
mode: 'Ask Bee',
text: 'Call the customer to confirm the move date.',
attributes: ['TRIGGER', 'SYSTEM', 'AI_RESPONSE_REQUESTED'],
});
// Log system action
addCommentToJob({
mode: 'System',
text: 'Script closed job due to opt-out.',
});
Tags
hasTag(ctx: ScriptContext, tagName: string): boolean
Checks if the job has a tag.
if (hasTag(ctx, 'Opt Out')) {
return;
}
addTag(ctx: ScriptContext, tagName: string)
Adds a tag to the job.
addTag(ctx, 'New Lead');
removeTag(ctx: ScriptContext, tagName: string)
Removes a tag from the job.
removeTag(ctx, 'New Lead');
Job Status
closeJob(reason: string)
Closes the job with the specified reason.
closeJob('could_not_contact');
closeJob('opt_out');
Scheduling
schedule(triggerName: string, delay: string | number, data: object, ctx: ScriptContext, opts?: { key?: string })
Schedules a future hook execution.
Delay formats: '5 min', '2 hours', '1 day', or milliseconds.
// One-time callback
schedule('followup', '3 hours', { attempt: 1 }, ctx);
// Rolling daily check (key overwrites previous)
schedule('daily-check', '24 hours', { day: 1 }, ctx, { key: 'daily' });
Charges
addCharge(input: { productName?: string; amount?: number })
Adds a charge to the job. Idempotent by product name.
addCharge({
productName: 'Fuel Surcharge',
amount: 75,
});
deleteCharge(chargeId: string)
Deletes a charge.
deleteCharge('charge-123');
Zone Setup (install hook only)
createZoneTag(tagName: string): Promise<boolean>
Creates a tag in the zone. Returns true if created, false if exists.
await createZoneTag('New Lead');
Logging
log(...args: any[])
Logs to the server-side logger and returns to frontend for install hooks.
log('Script started', job.code);
Injected Globals
__branding
Zone branding config, pre-fetched before execution.
const { companyName, phone, primaryColor, logoUrl } = __branding;
sendSMS({
body: `Hi! This is ${companyName}. Call us at ${phone}.`,
});
Available: companyName, appName, tagline, primaryColor, secondaryColor, logoUrl, logoSquareUrl, website, phone, email, address, aiName, businessHours.
jobCharges
Array of all charges across job events.
const hasFuelSurcharge = jobCharges.some(c => c.productName === 'Fuel Surcharge');
TSX Email Templates
Scripts can use JSX/TSX for HTML emails. A minimal runtime is injected:
/** @jsx h */
/** @jsxFrag Fragment */
function WelcomeEmail({ name, branding }) {
return (
<table width="600" style={{ margin: '0 auto' }}>
<tr>
<td style={{ padding: '24px' }}>
<h2 style={{ color: branding.primaryColor }}>Hi {name}!</h2>
<p>Welcome to {branding.companyName}.</p>
</td>
</tr>
</table>
);
}
hook('job-created', (ctx) => {
const html = renderToString(<WelcomeEmail name={firstName(ctx)} branding={__branding} />);
sendEmail({ subject: 'Welcome!', html });
});
Style props support both objects { fontSize: '14px' } and strings "font-size: 14px".
Sample Scripts
Web Lead Followup
5-day automated sequence for online leads (OBE, Website, Lead Aggregator):
- Ask Bee autodial on creation
- Welcome SMS after 3 minutes
- Daily Ask Bee follow-ups
- TSX email templates with moving tips
- Auto-close after 5 days
Estimate Followup
5-day sequence after estimate sent:
- Daily Ask Bee check-ins
- Value proposition emails
- Escalation to sales lead
Valuation Coverage
Automated valuation selection:
- Sends coverage options email when job is booked
- Handles customer click to add charge
- Sends confirmation email
Type Declarations
Full type definitions at script.d.ts.
Best Practices
- Filter early — Return early for irrelevant triggers
- Use install hooks — Document setup requirements
- Schedule with keys — Prevent duplicate pending jobs
- Close jobs explicitly — Use
closeJob(reason)when sequences end - Personalize with
__branding— Use zone-specific names, colors, contact info - Add AI_RESPONSE_REQUESTED — For Ask Bee comments to work properly
Restrictions
- No
call()function — Can’t directly call Moleculer services - SSRF protection —
fetch()can’t reach private IPs - 5 fetch calls max per execution
- 60 second timeout per script
- No filesystem access — Runs in sandboxed QuickJS VM
Getting Help
- Documentation: This page
- Type definitions: script.d.ts
- Sample scripts: functions.moveright.app/scripts
- Support: Contact MoveRight support for script development assistance