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 an event handler (runs once when issue changes):
const props = await issue.getEntityProperties();
await props.setInteger('subtask-count', subtasks.length);

// In a smart field (runs on every view - just reads):
const count = await props.getInteger('subtask-count');
return count || 0;

Error Handling

Event handlers Should Be Resilient

Event handlers 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 an event handler modifies an issue, which triggers another event, which fires the same event handler - it's detected and stopped.

Avoid:

  • An event handler on "issue updated" that updates the same issue unconditionally
  • Post-functions that trigger events caught by event handlers 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 Activity Log. 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
Smart field is slow Move computation to an event handler, 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