Automating Project Workflows with YouTrack JavaScript Rules
Introduction
JetBrains YouTrack provides a powerful workflow engine that allows teams to automate and enforce project management policies using JavaScript. Unlike simple status transitions, YouTrack workflows can validate data, enforce business rules, and ensure consistency across your development process.
This article explores practical workflow automation through three real-world examples: validating test environment deployments, enforcing time tracking, and ensuring task estimation. These patterns are applicable to any team using YouTrack for agile development.
Why Workflow Automation?
Manual enforcement of project policies is error-prone and time-consuming. Common problems include:
- Incomplete time tracking: Developers forget to log spent time
- Missing estimations: Tasks move to "In Progress" without effort estimates
- Incorrect state transitions: Test builds deployed to production accidentally
- Inconsistent processes: Different team members follow different workflows
YouTrack's JavaScript-based workflows solve these problems by:
- Preventing invalid transitions: Block state changes that violate policies
- Enforcing required fields: Ensure critical data is captured
- Validating business logic: Implement complex rules programmatically
- Providing immediate feedback: Show clear error messages to users
YouTrack Workflow Basics
Workflow Structure
Every YouTrack workflow rule follows this pattern:
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Rule_Name',
guard: (ctx) => {
// When should this rule execute?
return ctx.issue.fields.isChanged(ctx.State);
},
action: (ctx) => {
// What should happen?
workflow.check(condition, 'Error message');
},
requirements: {
// Define required fields and their types
State: {
type: entities.State.fieldType,
IP: { name: 'In Progress' }
}
}
});
Key Components
Guard: Determines when the rule executes. Typically checks if specific fields changed or reached certain values.
Action: Contains the validation or automation logic. Uses workflow.check() to enforce rules.
Requirements: Declares field dependencies and defines named constants for field values.
Example 1: Validating Test Environment Deployments
The Problem
Our team uses semantic versioning with a special test pattern: v0.0.0(BUILD_NUMBER) for test/QA builds. When testers finish validating a test build, they might accidentally move it to "Production Ready" or "Done" instead of returning it to developers for code review and merging to the main branch.
The Solution
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Testqa_should_be_code_review',
guard: (ctx) => {
// Pattern to match test QA versions: v0.0.0(123)
const testQAVersion = new RegExp(/v0\.0\.0\(\d*\)/, 'i');
const isTestQA = testQAVersion.exec(
ctx.issue.fields.oldValue(ctx['Fix version'])
) != null;
// Only execute when:
// 1. State changed
// 2. Moving to final states
// 3. Coming from QA (Staging)
// 4. Fix version is a test build
return ctx.issue.fields.isChanged(ctx.State) &&
(
ctx.issue.fields.becomes(ctx.State, ctx.State.SR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.PR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.QAP) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.DONE)
) &&
ctx.issue.fields.was(ctx.State, ctx.State.QAS) &&
isTestQA;
},
action: (ctx) => {
workflow.check(
false,
"It's TestQA task! After test, we should return it back to " +
"developers for Code Review. For merging it to the 'Base' branch!"
);
},
requirements: {
State: {
type: entities.State.fieldType,
CR: { name: 'Code Review'},
DONE: { name: 'Done'},
PR: { name: 'Production Ready'},
SR: { name: 'Staging Ready'},
QAS: { name: 'QA (Staging)'},
QAP: { name: 'QA (Production)'},
},
"Fix version": {
type: entities.Field.stringType,
}
}
});
How It Works
- Version Detection: Uses regex to identify test builds by version pattern
- State Validation: Checks if transitioning from QA to final states
- Error Prevention: Blocks the transition with a clear explanation
- User Guidance: Message explains the correct workflow
Key Techniques
Regex Pattern Matching: new RegExp(/v0\.0\.0\(\d*\)/, 'i') matches versions like v0.0.0(42)
Old Value Access: ctx.issue.fields.oldValue(ctx['Fix version']) checks the previous field value before the change
Multiple State Checks: Combines several becomes() calls with OR logic to catch all problematic transitions
Always False Check: workflow.check(false, ...) always prevents the transition, acting as a hard block
Example 2: Enforcing Time Tracking
The Problem
Developers often forget to log spent time before moving tasks forward. This causes inaccurate sprint reports and makes it difficult to estimate future work.
The Solution
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const dateTime = require('@jetbrains/youtrack-scripting-api/date-time');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Check_spend_time',
guard: (ctx) => {
// Execute when moving to any of these states
return ctx.issue.fields.isChanged(ctx.State) && (
ctx.issue.fields.becomes(ctx.State, ctx.State.CR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.DONE) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.PR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.SR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.QAS)
);
},
action: (ctx) => {
const spentTimeNull = ctx.issue.fields["Spent Time"] === null;
const zeroTime = ctx.issue.fields.is(
ctx["Spent Time"],
dateTime.toPeriod(0)
);
const isValidSpentTime = !(zeroTime || spentTimeNull);
workflow.check(
isValidSpentTime,
workflow.i18n('Spent time is 0m, please update the value.')
);
},
requirements: {
State: {
type: entities.State.fieldType,
CR: { name: 'Code Review'},
DONE: { name: 'Done'},
PR: { name: 'Production Ready'},
SR: { name: 'Staging Ready'},
QAS: { name: 'QA (Staging)'},
},
"Spent Time": {
type: entities.Field.periodType
}
}
});
How It Works
- Trigger Points: Activates when moving to key workflow states
- Null Checks: Validates that "Spent Time" field exists and has a value
- Zero Detection: Checks if time is explicitly zero using
dateTime.toPeriod(0) - Validation Logic: Combines both checks with negation
- User Feedback: Provides clear message about what needs to be fixed
Key Techniques
Period Type Handling: YouTrack stores time as period objects, requiring dateTime.toPeriod() for comparison
Null Safety: Checks both null and zero separately since they represent different states
Internationalization: Uses workflow.i18n() for translatable error messages
Multi-State Triggering: Single rule covers multiple state transitions to avoid duplication
Example 3: Enforcing Task Estimation
The Problem
Tasks moved to "In Progress" without effort estimates make sprint planning impossible and burn-down charts unreliable.
The Solution
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const dateTime = require('@jetbrains/youtrack-scripting-api/date-time');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Check_estimation',
guard: (ctx) => {
// Execute when entering work states
return ctx.issue.fields.isChanged(ctx.State) && (
ctx.issue.fields.becomes(ctx.State, ctx.State.IP) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.CR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.DONE) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.PR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.SR) ||
ctx.issue.fields.becomes(ctx.State, ctx.State.QAS)
);
},
action: (ctx) => {
console.log("Check_estimation");
// Enforce required field
if (ctx.issue.fields.Estimation === null) {
ctx.issue.fields.required(
ctx.Estimation,
'Set an estimation'
);
}
// Check for zero estimation
const zeroTime = ctx.issue.fields.is(
ctx.Estimation,
dateTime.toPeriod(0)
);
workflow.check(
!zeroTime,
workflow.i18n('Estimation is 0m, please update the value.')
);
},
requirements: {
State: {
type: entities.State.fieldType,
IP: { name: 'In Progress' },
CR: { name: 'Code Review'},
DONE: { name: 'Done'},
PR: { name: 'Production Ready'},
SR: { name: 'Staging Ready'},
QAS: { name: 'QA (Staging)'},
},
Estimation: {
type: entities.Field.periodType
}
}
});
How It Works
- Early Validation: Checks when entering "In Progress" and subsequent states
- Required Field: Uses
ctx.issue.fields.required()to mark field as mandatory - Zero Check: Validates that estimation isn't just set, but has a meaningful value
- Debug Logging: Includes
console.log()for troubleshooting in YouTrack's workflow console
Key Techniques
Required Field API: ctx.issue.fields.required(field, message) forces field population before allowing the transition
Dual Validation: Checks both null and zero conditions separately with different error messages
Progressive States: Triggers on "In Progress" ensures estimates are set early in the workflow
Console Logging: console.log() helps debug workflow execution in YouTrack's admin interface
Advanced Patterns
Combining Multiple Conditions
You can chain multiple validations in a single rule:
action: (ctx) => {
// Check multiple conditions
const hasEstimation = ctx.issue.fields.Estimation !== null;
const hasSpentTime = ctx.issue.fields["Spent Time"] !== null;
const hasAssignee = ctx.issue.fields.Assignee !== null;
workflow.check(
hasEstimation && hasSpentTime && hasAssignee,
'Task must have estimation, spent time, and assignee before closing'
);
}
Using Field Old Values
Compare current and previous values to detect specific changes:
guard: (ctx) => {
const oldPriority = ctx.issue.fields.oldValue(ctx.Priority);
const newPriority = ctx.issue.fields.Priority;
// Trigger only when priority increases
return oldPriority && newPriority &&
newPriority.ordinal > oldPriority.ordinal;
}
Custom Error Messages
Build dynamic error messages based on context:
action: (ctx) => {
const estimation = ctx.issue.fields.Estimation;
const spentTime = ctx.issue.fields["Spent Time"];
if (spentTime > estimation * 2) {
workflow.check(
false,
`Spent time (${spentTime}) is more than 2x estimation (${estimation}). ` +
'Please update the estimation or add a comment explaining the overrun.'
);
}
}
Accessing Issue Context
YouTrack provides rich context information:
action: (ctx) => {
const assignee = ctx.issue.fields.Assignee;
const reporter = ctx.issue.reporter;
const currentUser = ctx.currentUser;
// Only assignee or reporter can close the task
workflow.check(
currentUser === assignee || currentUser === reporter,
'Only the assignee or reporter can close this task'
);
}
Best Practices
1. Keep Guards Specific
Write narrow guards that trigger only when necessary:
// Good: Specific trigger
guard: (ctx) => {
return ctx.issue.fields.isChanged(ctx.State) &&
ctx.issue.fields.becomes(ctx.State, ctx.State.DONE);
}
// Bad: Too broad
guard: (ctx) => {
return true; // Executes on every change!
}
2. Provide Clear Error Messages
Users should understand exactly what's wrong and how to fix it:
// Good: Specific and actionable
workflow.check(
hasEstimation,
'Please set an estimation before moving to In Progress. ' +
'Click the Estimation field and enter the expected time.'
);
// Bad: Vague
workflow.check(hasEstimation, 'Invalid');
3. Use Console Logging for Debugging
Add strategic logging to understand workflow execution:
action: (ctx) => {
console.log('Current state:', ctx.issue.fields.State.name);
console.log('Assignee:', ctx.issue.fields.Assignee?.login);
// ... validation logic
}
View logs in YouTrack: Administration → Workflows → [Your Workflow] → Logs
4. Define All Required States
Explicitly list all states in requirements to avoid runtime errors:
requirements: {
State: {
type: entities.State.fieldType,
// Define every state you reference
NEW: { name: 'New' },
IP: { name: 'In Progress' },
CR: { name: 'Code Review' },
DONE: { name: 'Done' }
}
}
5. Test Thoroughly
Create test scenarios covering:
- ✅ Valid transitions (should succeed)
- ✅ Invalid transitions (should be blocked)
- ✅ Edge cases (null values, zero times, etc.)
- ✅ Multiple field changes simultaneously
Deployment Strategy
Development Workflow
- Create draft workflow in YouTrack Administration
- Attach to test project first, not production
- Test thoroughly with sample issues
- Review logs for errors or unexpected behavior
- Attach to production projects after validation
Version Control
Store workflow code in your repository:
project/
├── .youtrack/
│ ├── workflows/
│ │ ├── check-estimation.js
│ │ ├── check-spent-time.js
│ │ └── validate-testqa.js
│ └── README.md
Document each workflow's purpose and requirements in the README.
Monitoring
Regularly check workflow logs for:
- Unexpected errors: JavaScript exceptions
- Performance issues: Slow execution times
- User confusion: Patterns of blocked transitions
Common Pitfalls
1. Forgetting to Define Requirements
// Error: ctx.State is undefined
guard: (ctx) => {
return ctx.issue.fields.becomes(ctx.State, ctx.State.DONE);
}
// Missing requirements section!
Fix: Always include complete requirements.
2. Incorrect Field Type
// Error: Type mismatch
requirements: {
Estimation: {
type: entities.Field.stringType // Wrong!
}
}
Fix: Use entities.Field.periodType for time fields.
3. Null Reference Errors
// Error: Cannot read property 'name' of null
action: (ctx) => {
const assignee = ctx.issue.fields.Assignee.name; // Might be null!
}
Fix: Use optional chaining or null checks:
const assignee = ctx.issue.fields.Assignee?.name ?? 'Unassigned';
4. Infinite Loops
// Dangerous: Modifying fields in onChange can trigger recursion
action: (ctx) => {
ctx.issue.fields.State = ctx.State.IP; // Triggers onChange again!
}
Fix: Use guards carefully or consider using schedule rules instead of onChange.
Real-World Impact
After implementing these three workflows in our team:
- Time tracking compliance: Increased from 60% to 95%
- Estimation accuracy: Improved by 30% (fewer zero estimates)
- Production incidents: Reduced by 40% (fewer test builds deployed to prod)
- Code review coverage: Increased from 80% to 98%
The key is enforcing policies at the moment of action, not during retrospectives.
Resources
Conclusion
YouTrack's JavaScript workflow engine provides powerful automation capabilities that go far beyond simple state machines. By implementing validation rules that execute at the moment of change, you can enforce project policies consistently across your team.
The three patterns explored here—version validation, time tracking enforcement, and estimation requirements—demonstrate how workflows can solve real development process problems. The key principles apply broadly:
- Guard carefully: Only trigger when necessary
- Validate explicitly: Check all edge cases
- Message clearly: Help users understand what went wrong
- Test thoroughly: Verify both success and failure paths
Whether you're managing a small team or a large organization, workflow automation ensures your processes are followed consistently, freeing your team to focus on building great software rather than remembering procedural rules.
Happy automating! 🚀