Prerequisites
Before you begin development:- macOS with OmniFocus installed
- Node.js (for linting only, not required for the plugin itself)
- Jira Instance with API access for testing
- Text Editor (VS Code, Sublime Text, etc.)
- Console.app for viewing debug logs
Plugin Architecture
OmniFocus Plugin Structure
OmniFocus plugins follow a specific directory structure:- The plugin bundle directory must end with
.omnifocusjs manifest.jsondefines available actions and their metadata- Each action is an individual JavaScript file in
Resources/ jiraCommon.jsis a shared library used by all actions
Manifest Structure
Themanifest.json file defines the plugin:
Resources/ (e.g., configureJira.js).
Action File Structure
All action files follow this pattern:- IIFE (Immediately Invoked Function Expression): Wraps the entire file to avoid global scope pollution
- PlugIn.Action: Constructor that takes an async function as the action handler
- validate: Optional function that determines when the action should be enabled
- return action: Returns the action object to OmniFocus
Core Components
1. configureJira.js
Provides the configuration interface for setting up Jira credentials and sync settings. Key Functions:- Displays a form with input fields for Jira URL, credentials, JQL query, tag name, etc.
- Tests the connection to Jira before saving
- Stores credentials in macOS Keychain via the Credentials API
- Stores settings in OmniFocus Preferences
jiraUrl: Jira instance URLjqlQuery: JQL filter querytagName: OmniFocus tag for synced taskslastSyncTime: Timestamp of last successful synccompletedStatuses: Custom completed status mappings (optional)droppedStatuses: Custom dropped status mappings (optional)enableProjectOrganization: Enable automatic project creationdefaultProjectFolder: Folder path for new projects (optional)
user: Jira account IDpassword: Jira API token
2. syncJira.js
Performs incremental synchronization (fetches only recently updated issues). Key Features:- Appends
AND updated >= "[lastSyncTime]"to JQL query - Creates new tasks for new issues
- Updates existing tasks for modified issues
- Reopens completed/dropped tasks if Jira status changes
- Updates
lastSyncTimeafter successful sync
- Load settings and credentials
- Build JQL query with time filter
- Fetch issues from Jira (with pagination)
- For each issue:
- Find existing task by
[JIRA-KEY]prefix - Create or update task
- Apply status mappings
- Find existing task by
- Display sync statistics
3. syncJiraFull.js
Performs full refresh synchronization (fetches all matching issues and cleans up orphaned tasks). Key Features:- Fetches all issues matching base JQL query (no time filter)
- Creates/updates tasks like incremental sync
- Additionally: Marks tasks as completed if they no longer appear in Jira results
- Useful for cleanup when issues move out of scope or are deleted
- Fetch all issues matching JQL query
- Build a set of Jira keys that should exist
- Find all OmniFocus tasks with the configured tag
- For tasks with
[JIRA-KEY]prefix not in the result set:- Mark as completed
- Log as “completed”
4. jiraCommon.js
Shared library providing common functionality used by all actions. Key Functions:Settings Management
Credentials Management
API Communication
Task Management
Project Management
Data Conversion
Error Handling
Key Implementation Details
Task Identification
Tasks are identified by the[JIRA-KEY] prefix in the task name:
Status Mapping
Jira statuses map to OmniFocus task states:ADF (Atlassian Document Format) Conversion
Jira descriptions use Atlassian Document Format (JSON). TheconvertAdfToMarkdown() function recursively processes the ADF tree:
Supported Elements:
- Paragraphs, headings (h1-h6)
- Unordered and ordered lists (with nesting)
- Code blocks and inline code
- Text formatting: bold, italic, strikethrough, underline
- Links, mentions, emoji
- Blockquotes, horizontal rules
Base64 Encoding
Sincebtoa() is not available in OmniFocus JavaScript, the plugin implements custom Base64 encoding:
Retry Logic with Exponential Backoff
Retry-After header (up to 60s max).
Pagination
Jira API responses are paginated (100 issues per page):Incremental Sync Time Filtering
For incremental sync, the plugin appends a time filter to the JQL query:YYYY-MM-DD HH:MM (Jira JQL date format)
Performance Optimization
The plugin uses indexed lookups for O(1) task/project searches:OmniFocus API Reference
Available APIs
Preferences API:Limitations
Not Available:btoa()/atob()(use custom Base64 implementation)fetch()/XMLHttpRequest(useURL.FetchRequest)- Node.js modules (
fs,path, etc.) - Browser APIs (
window,document, etc.) - ES modules (use IIFEs instead)
task.containingProject(cannot move tasks between projects after creation)- Most properties are read-only after object creation
Testing
Manual Testing
There is no automated test suite. Testing is done manually:- Make changes to plugin files
- Copy the plugin to the OmniFocus Plug-Ins directory:
- Completely restart OmniFocus (⌘Q, then relaunch)
- Run Configure JIRA Sync to set up test credentials
- Run Sync Jira or Sync Jira Full to test sync functionality
- Check Console.app for debug output:
- Open Console.app
- Filter for “OmniFocus”
- Look for
console.log()output and errors
Debugging with Console.app
Useconsole.log() statements liberally:
Test Scenarios
Configuration Testing:- Invalid Jira URL
- Invalid credentials
- Invalid JQL query
- Valid configuration
- First sync (all new tasks)
- Incremental sync (some updated, some unchanged)
- Full sync with cleanup (some issues removed)
- Status changes (active → completed → reopened)
- Due date changes
- Parent issue changes (project organization)
- Empty JQL results
- Large result sets (100+ issues)
- Issues with no description
- Issues with complex ADF formatting
- Network failures (simulate by disconnecting Wi-Fi)
- Rate limiting (simulate by making many requests quickly)
Linting
The project uses ESLint for code quality:.eslintrc.json):
Common Development Tasks
Adding a New Action
- Create a new JavaScript file in
Resources/(e.g.,myAction.js) - Follow the action file structure (IIFE returning
PlugIn.Action) - Add the action to
manifest.json: - Restart OmniFocus to load the new action
Adding a New Setting
- Add a form field in
configureJira.js: - Save the setting:
- Use the setting in sync actions:
Modifying Status Mappings
Status mappings are defined injiraCommon.js:
Adding New Jira Fields
To sync additional Jira fields:- Add the field to the fetch request:
- Access the field in task creation/update:
Architecture Decisions
Why IIFE Pattern?
OmniFocus requires each action file to return aPlugIn.Action object. The IIFE pattern:
- Avoids polluting the global scope
- Allows local variables and functions
- Returns the action object to OmniFocus
Why Shared Library?
jiraCommon.js is a shared library used by all actions:
- Avoids code duplication
- Provides consistent behavior across actions
- Simplifies maintenance
Why Task Prefix?
Using[JIRA-KEY] prefix for task identification:
- Simple and reliable
- Visible to users (helps identify synced tasks)
- Works with OmniFocus search and filtering
- No need for hidden metadata or custom fields
Why Incremental Sync?
Default incremental sync improves performance:- Reduces API calls to Jira
- Faster sync times
- Lower risk of rate limiting
- Sufficient for daily use
Why Read-Only?
The plugin never modifies Jira:- Simpler implementation
- Lower risk of data loss
- Easier permission management
- Jira remains the single source of truth
- More complex conflict resolution
- Higher permissions in Jira
- Risk of accidental issue modifications