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() or makeRequest() 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 information
  • console.warn() — unexpected but handled situations
  • console.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