Imported from harpchad/jira-omnifocus-improved (
AGENTS.md). Install upstream withnpx skills add harpchad/jira-omnifocus-improved. Copyright stays with the author.
AI Agent Context: Jira-OmniFocus Plugin
This document provides context for AI coding agents working on the jira-omnifocus-improved project.
⚠️ Important: See TEST-RESULTS.md for verified JavaScript capabilities. Earlier versions of this document contained incorrect assumptions about const/let limitations that have been corrected.
Project Overview
Type: OmniFocus plugin (JavaScript)
Purpose: Sync Jira issues to OmniFocus tasks
Runtime: OmniFocus Automation/Omni Automation (JavaScript for Automation)
Platform: macOS, iOS, iPadOS
Key Constraints
JavaScript Environment
✅ Good news! OmniFocus supports modern JavaScript (ES6+). You can use:
constandlet(preferred overvar)- Arrow functions
- Template literals
- Destructuring, spread operators
- Array methods (map, filter, find, forEach)
- Promises
- All modern JavaScript features
The ONE Critical Constraint
❌ Credentials objects MUST be instantiated at plugin load time
(() => {
// ✅ CORRECT: Create Credentials at top of IIFE (plugin load time)
const credentials = new Credentials();
const action = new PlugIn.Action((selection, sender) => {
// ✅ Use the credentials instance here
const cred = credentials.read("my-key");
// ❌ WRONG: Cannot create new Credentials here!
// const newCreds = new Credentials(); // Error: "Credential objects may only be constructed when loading a plug-in"
});
return action;
})();
Why? The OmniFocus Credentials API requires the object to be created during plugin initialization, not during action execution. This is a security/lifecycle constraint, not a JavaScript limitation.
Note: Our earlier code used var everywhere due to incorrect assumptions during troubleshooting. Modern JavaScript (const/let/arrows) works perfectly. See TEST-RESULTS.md for proof.
API Constraints
-
Jira Cloud API v3 - Uses
/rest/api/3/search/jqlendpoint- MUST include
fieldsparameter or response only contains IDs - Example:
?jql=query&fields=key,summary,description,duedate
- MUST include
-
Atlassian Document Format (ADF) - Jira descriptions use structured format
- Not plain text strings
- Must recursively extract text from nested content nodes
- See
extractTextFromADF()function
-
OmniFocus Credentials API - Secure storage in macOS Keychain
const credentials = new Credentials(); // MUST be at plugin load time (top of IIFE) const credential = credentials.read(credentialKey); credentials.write(credentialKey, user, password); credentials.remove(credentialKey);
Architecture
Core Flow
-
Credential Management
- Check for existing credentials in Keychain
- Prompt user if missing
- Store securely (never in plugin file)
-
API Request
- Build JQL query with optional fields (summary, description, duedate)
- Authenticate with Basic Auth (username + API token)
- Fetch issues from Jira Cloud API v3
-
Task Synchronization
- Existing tasks: Mark incomplete, update due dates if enabled
- New tasks: Create in OmniFocus inbox with tag, note, optional due date
- Removed tasks: Mark complete if no incomplete children
-
Error Handling
- HTTP errors with specific messages (401, 403, 404, 410)
- Auto-clear invalid credentials on 401
- JSON parsing errors
- Date parsing errors
Key Functions
extractTextFromADF(doc)
Recursively extracts plain text from Atlassian Document Format.
Input: ADF object { type: 'doc', content: [...] }
Output: Plain text string with paragraph breaks
Purpose: Convert Jira's rich text descriptions to plain text for OmniFocus notes
Main Action Flow
- Retrieve/prompt credentials
- Build API URL with fields parameter
- Fetch issues from Jira
- Parse response (handle both old string and new ADF formats)
- Sync tasks (create/update/complete)
- Show success/failure alert
Configuration Options
var jiraUrl = "https://company.atlassian.net"; // Jira instance URL
var credentialKey = "jira-default"; // Keychain identifier
var omnifocusTagToUse = "jira"; // Tag for synced tasks
var jiraQuery = "assignee=currentuser() and resolution is empty"; // JQL
var processComments = false; // Comment sync (legacy, disabled)
var omnifocusCommentTagToUse = "Jira-Comment"; // Comment tag (legacy)
var syncDueDates = false; // Sync Jira due dates to OmniFocus
syncDueDates Behavior
When true:
- Requests
duedatefield from API - Sets
newTask.dueDatefor new tasks - Updates
existingTask.dueDatefor existing tasks - Clears
dueDateif Jira issue has none
When false (default):
- Doesn't request duedate field (slightly faster API calls)
- No due date operations
Common Issues & Solutions
Issue: Tasks showing only numbers (IDs)
Cause: Missing fields parameter in API request
Solution: Always include &fields=key,summary,description (plus duedate if enabled)
Issue: Descriptions are [object Object]
Cause: ADF format not being parsed
Solution: Check if description.type === 'doc' and use extractTextFromADF()
Issue: "Credential objects may only be constructed when loading a plug-in"
Cause: Trying to create new Credentials() inside the action function
Solution: Move Credentials instantiation to top of IIFE (plugin load time)
// ✅ CORRECT
(() => {
const credentials = new Credentials(); // At plugin load time
const action = new PlugIn.Action((selection, sender) => {
const cred = credentials.read("key");
});
return action;
})();
// ❌ WRONG
(() => {
const action = new PlugIn.Action((selection, sender) => {
const credentials = new Credentials(); // Error!
});
return action;
})();
Issue: HTTP 401 errors
Cause: Invalid/expired API token
Solution: Plugin auto-clears credentials, prompts user to re-enter
Issue: Due dates not syncing
Cause: syncDueDates is false or duedate field not requested
Solution: Set syncDueDates = true and verify fields parameter includes duedate
Testing Guidelines
Test Scenarios
-
First Run
- No credentials → Should prompt
- Save credentials → Should store in Keychain
- Run again → Should not re-prompt
-
API Connectivity
- Valid credentials → Success alert with counts
- Invalid credentials → 401 error, credentials cleared, user prompted
- Network error → Clear error message
-
Task Creation
- New Jira issue → Creates OmniFocus task
- Task name format:
PROJECT-123 Issue Summary - Task note: URL + description (plain text from ADF)
- Task tags: Includes configured tag
-
Task Updates
- Existing task → Marks incomplete
- With
syncDueDates=true→ Updates due date - Jira issue removed → Marks complete (if no children)
-
Due Date Syncing (when enabled)
- Jira issue with due date → OmniFocus task gets due date
- Existing task, date changed → OmniFocus task updated
- Jira due date removed → OmniFocus due date cleared
Debug Logging
Console.app shows debug output:
DEBUG: Fetching URL: ...
DEBUG: User: ...
DEBUG: Response status: 200
DEBUG: Response MIME type: application/json
DEBUG: Response keys: issues, isLast
DEBUG: Found 5 issues
DEBUG: First issue keys: id, key, self, fields
DEBUG: Set due date for PROJECT-123: Fri Nov 22 2025...
Code Style
- Variables: Use
constfor immutable values,letfor mutable, descriptive names - Constants: Uppercase with underscores (e.g.,
HTTP_UNAUTHORIZED) - Functions: Arrow functions preferred for callbacks, traditional for standalone functions
- Arrays: Use modern methods (map, filter, find, forEach) over traditional for loops
- Strings: Template literals preferred over concatenation
- Error handling: Try-catch with specific error messages
- Logging: Use
console.log()for DEBUG,console.error()for ERROR - Alerts: User-friendly messages with actionable guidance
Note: The existing code uses var throughout due to earlier incorrect assumptions. Modern JavaScript works fine. Use const/let for new code.
File Structure
jira.omnifocusjs # Main plugin file
├── Metadata (lines 1-10) # Plugin info, version, identifier
├── Helper Functions # extractTextFromADF()
├── Action Function # Main sync logic
│ ├── Configuration # User-editable settings
│ ├── Constants # API paths, HTTP codes
│ ├── Credential Mgmt # Keychain operations
│ ├── API Request # Build URL, fetch issues
│ ├── Response Parsing # Handle Jira API response
│ ├── Task Sync # Create/update/complete tasks
│ └── Error Handling # Catch and display errors
└── Validation Function # Always return true
API Reference
Jira Cloud API v3
Endpoint: GET /rest/api/3/search/jql
Auth: Basic (email + API token)
Query Params:
jql: JQL query string (URL encoded)fields: Comma-separated field list (REQUIRED)
Response:
{
"issues": [
{
"id": "12345",
"key": "PROJECT-123",
"fields": {
"summary": "Issue title",
"description": {
"type": "doc",
"content": [...] // ADF format
},
"duedate": "2025-11-30" // ISO 8601 date
}
}
],
"isLast": true
}
OmniFocus Automation API
Task Creation:
var task = new Task(name, inbox.beginning);
task.addTag(tag);
task.note = "text";
task.dueDate = new Date("2025-11-30");
Task Modification:
existingTask.markIncomplete();
existingTask.markComplete();
existingTask.dueDate = null; // Clear due date
Tags:
var tag = tags.byName("tagName") || new Tag("tagName");
Version History
- v2.3: Added due date syncing feature
- v2.2: Migrated to API v3, added ADF parser, fixed field selection
- v2.0: Security improvements, Keychain storage, error handling
Resources
Agent Instructions
When modifying this plugin:
- Always use
varfor top-level declarations - Test credential flow - first run, re-authentication
- Verify field selection - include all needed fields in API request
- Handle ADF format - check
typeofanddoc.typebefore parsing - Add debug logging - help users troubleshoot issues
- Update version number - in metadata when making changes
- Preserve backward compatibility - default new features to
false - Test with actual Jira - curl commands with real API tokens
- Document configuration - add comments for new options
- Update README.md - reflect new features/changes
Common Enhancement Patterns
Adding a New Configuration Option
- Add to configuration section with comment
- Add default value (usually
falsefor new features) - Use in conditional logic throughout sync
- Update README.md Configuration section
- Update version number
- Commit with descriptive message
Adding a New Jira Field to Sync
- Add field name to
fieldsvariable (conditionally if optional) - Parse field from
issue.fields.fieldname - Set on OmniFocus task (new and existing)
- Add error handling for parsing
- Add debug logging
- Test with issues that have/don't have the field
Improving Error Handling
- Identify error scenario (HTTP code, parse error, etc.)
- Add specific catch or check
- Create user-friendly error message
- Log detailed error to console
- Provide actionable guidance (e.g., "Check credentials")
- Test error scenario