Best Practices
Guidelines for writing efficient, maintainable, and reliable ScriptForge scripts.
Script Performance
Keep Scripts Fast
- The runtime budget is 22 seconds per execution cycle. Scripts exceeding this are terminated.
- Script size limit is 100KB. Keep code concise.
- Avoid unnecessary API calls — each
WorkItems.getByKey()ormakeRequest()adds latency.
Minimize API Calls
// ❌ Bad — fetches each issue individually
const keys = ['PROJ-1', 'PROJ-2', 'PROJ-3'];
for (const key of keys) {
const issue = await WorkItems.getByKey(key);
// process...
}
// ✅ Better — use JQL search to fetch in bulk
const results = await WorkItems.search('key in (PROJ-1, PROJ-2, PROJ-3)');
await results.forEach(issue => {
// process...
});
Use Entity Properties for Caching
For data that's expensive to compute, store it via entity properties and read it when needed:
// In a listener (runs once when issue changes):
const props = await issue.getEntityProperties();
await props.setInteger('subtask-count', subtasks.length);
// In a scripted field (runs on every view — just reads):
const count = await props.getInteger('subtask-count');
return count || 0;
Error Handling
Listeners Should Be Resilient
Listeners fire on events — if your script fails, the event still proceeds. Handle errors gracefully:
try {
const issue = await WorkItems.getByKey(context.issueKey);
await issue.addComment('Processed by automation');
} catch (err) {
console.error(`Failed to process ${context.issueKey}: ${err.message}`);
// Don't throw — let the event continue
}
Workflow Conditions Fail Open
If a workflow condition script throws an error, the transition is allowed (fail-open). Always return explicit true or false:
// ✅ Explicit returns
const issue = await WorkItems.getByKey(context.issueKey);
const subtasks = await issue.getSubTaskObjects();
const allDone = subtasks.every(s => s.status === 'Done');
return allDone; // true or false
Workflow Validators Fail Closed
If a validator script throws an error, the transition is blocked (fail-closed). Make sure your validation logic is solid and handles edge cases.
Loop Detection
ScriptForge has a built-in loop detector. If a listener modifies an issue, which triggers another event, which fires the same listener — it's detected and stopped.
Avoid:
- A listener on "issue updated" that updates the same issue unconditionally
- Post-functions that trigger events caught by listeners that trigger more transitions
Pattern to avoid loops:
// Check before modifying to prevent re-triggers
const issue = await WorkItems.getByKey(context.issueKey);
const currentLabel = issue.labels || [];
if (!currentLabel.includes('processed')) {
await issue.update()
.setLabels([...currentLabel, 'processed'])
.execute();
}
Scheduled Jobs
Respect the Budget
The masterScheduler runs 3 jobs per cycle with a 22-second time budget. If your job processes many issues, use take() to limit batch size:
const results = await WorkItems.search('status = "Waiting for Review" AND updated < -7d');
const batch = await results.take(50); // Process max 50 per run
for (const issue of batch) {
await issue.addComment('This issue has been waiting for review for over 7 days.');
}
Idempotent Jobs
Scheduled jobs run repeatedly. Make them safe to re-run:
// ✅ Idempotent — checks before acting
const issue = await WorkItems.getByKey(key);
if (issue.status !== 'Reminded') {
await issue.addComment('Reminder: please update this issue');
}
Security
Validate Input
REST endpoints receive external data. Never trust it:
const body = JSON.parse(context.request.body);
if (!body.issueKey || typeof body.issueKey !== 'string') {
return { statusCode: 400, body: JSON.stringify({ error: 'Invalid issueKey' }) };
}
// Validate format
if (!/^[A-Z]+-\d+$/.test(body.issueKey)) {
return { statusCode: 400, body: JSON.stringify({ error: 'Invalid issue key format' }) };
}
Don't Log Sensitive Data
// ❌ Don't log tokens, credentials, or PII
console.log('Token:', token);
// ✅ Log identifiers only
console.log('Processing issue:', issueKey);
Code Organization
Use Helper Functions
async function getOrCreateLabel(issue, label) {
const labels = issue.labels || [];
if (!labels.includes(label)) {
await issue.update().setLabels([...labels, label]).execute();
}
}
Use console.log for Debugging
All output goes to Execution History. Use log levels meaningfully:
console.log()— normal flow informationconsole.warn()— unexpected but handled situationsconsole.error()— failures that need attention
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Script times out | Reduce API calls, process fewer items per run |
| Loop detection kills script | Add guards to prevent self-triggering |
| Scheduled job processes stale data | Use refresh() before acting on issue state |
| Scripted field is slow | Move computation to a listener, store in entity properties |
| Behaviour doesn't fire | Check project/issue type scope; can't use All + All |
| Validator unexpectedly blocks | Handle all error paths; validators fail-closed on errors |