| name | applescript-omnifocus |
| description | Use when writing, modifying, or debugging ANY AppleScript that interacts with OmniFocus. Also use when encountering AppleScript errors, unexpected OmniFocus behavior, or when adding new connector methods. Covers variable naming traps, rich text limitations, recurring task handling, date formats, string escaping, and batch operation patterns. |
AppleScript + OmniFocus Patterns
This skill covers the hard-won knowledge about scripting OmniFocus via AppleScript. These are not obvious from documentation and have caused real bugs in this project.
Critical: Variable Naming Conflicts
OmniFocus AppleScript has a dangerous quirk: if you use a variable name that matches an OmniFocus property, AppleScript silently reads the property instead of your variable.
Bug-causing names (NEVER use as variables):
name, note, status, flagged, completed, sequential, due date, defer date, estimated minutes, repetition rule, repetition method
Safe naming pattern: Always prefix with the entity type:
-- BAD: "name" collides with OmniFocus property
set name to "My Task"
set name of theTask to name -- reads property of theTask, not your variable!
-- GOOD: prefixed variable name
set taskName to "My Task"
set name of theTask to taskName -- works correctly
Real bug from this project: A variable called status inside a tell task block caused OmniFocus to read the task's status property instead of the variable value, producing silent data corruption. The elifintervalDays typo story: unit tests passed because they mocked AppleScript, but integration tests against real OmniFocus caught the variable collision. This is why integration tests exist.
Rich Text Notes Limitation
OmniFocus stores notes as rich text internally. The AppleScript API exposes this inconsistently:
- Reading:
note of task returns plain text (stripped of formatting)
- Writing:
set note of task to "text" replaces the entire note with plain text, destroying any rich text formatting
There is no workaround. You cannot preserve rich text through the AppleScript API. Document this to users and warn when overwriting notes.
Recurring Task Completion
Two ways to complete a task — only one handles recurrence correctly:
-- CORRECT: Creates next occurrence for recurring tasks
mark complete theTask
-- WRONG: Just marks this instance done, no next occurrence spawned
set completed of theTask to true
Use mark complete (the AppleScript command) for any task that might recur. Use set completed to true only when you specifically want to kill a recurring task's future occurrences.
The connector uses mark complete by default in update_task() when completed=True is passed.
JSON Construction in AppleScript
AppleScript has no native JSON support and no module system. Every AppleScript block that needs to return structured data must include inline JSON helper functions.
Current pattern (used 9x in the connector):
on escapeJSON(theText)
set resultText to ""
repeat with i from 1 to count of theText
set c to character i of theText
if c is "\"" then
set resultText to resultText & "\\\""
else if c is "\\" then
set resultText to resultText & "\\\\\\\\"
else
set resultText to resultText & c
end if
end repeat
return resultText
end escapeJSON
This duplication is intentional and necessary. Do not try to "DRY" it — AppleScript blocks must be self-contained because osascript -e executes each block independently.
Date Format Conversion
AppleScript uses locale-dependent date strings. The connector normalizes to ISO 8601:
- AppleScript format:
"March 5, 2026 5:00:00 PM"
- ISO 8601 format:
"2026-03-05T17:00:00"
- Conversion:
_iso_to_applescript_date() in the connector handles both directions
When writing new date-handling code, always use the existing converter rather than building date strings manually.
String Escaping
Always escape user-provided strings before embedding in AppleScript:
name_escaped = self._escape_applescript_string(name)
script = f'set name of theTask to "{name_escaped}"'
The _escape_applescript_string() method handles quotes and backslashes. Without this, a task name containing " will break the AppleScript block silently.
Batch Operation Patterns
Single AppleScript call for multiple items is always faster than multiple calls:
-- GOOD: One call, N items (100-300ms total)
tell application "OmniFocus"
tell front document
repeat with t in flattened tasks
-- process all tasks in one block
end repeat
end tell
end tell
-- BAD: N calls, one item each (100-300ms PER call)
for task_id in task_ids:
script = f'tell application "OmniFocus" ... get task {task_id} ...'
run_applescript(script) # 100-300ms each!
When batch is not possible: Some operations (like mark complete) must be done per-task because they trigger OmniFocus-internal workflows. In these cases, accept the overhead.
Error Handling Pattern
AppleScript errors should be caught in the script and returned as prefixed strings:
try
-- operation
return "SUCCESS"
on error errMsg
return "ERROR: " & errMsg
end try
The Python side then checks for the prefix:
result = run_applescript(script)
if result.startswith("ERROR:"):
raise Exception(result)
This is more reliable than catching subprocess.CalledProcessError, which doesn't always contain useful error messages from OmniFocus.
Database Safety System
The connector has a safety system that prevents destructive operations on production data:
- Requires
OMNIFOCUS_TEST_MODE=true and OMNIFOCUS_TEST_DATABASE=OmniFocus-TEST.ofocus
- Each destructive operation calls
_verify_database_safety() which runs an AppleScript to check the current database name
- Operations tracked in
DESTRUCTIVE_OPERATIONS set: create, update, delete, reorder
This adds ~100ms per destructive call but prevents catastrophic data loss. Do not bypass it.
Integration Test Requirement
If you wrote or modified AppleScript in the connector, you MUST write or update integration tests before the PR is ready. Unit tests mock run_applescript() — they prove your Python logic works but say nothing about whether the AppleScript is valid. Offer to run make test-integration before creating the PR.
OmniFocus-Specific AppleScript Quirks
first flattened task whose id is X — works for tasks. Use flattened projects for projects, folders for folders.
front document vs default document — front document is for UI operations, default document for data operations. Some operations require one or the other.
- Focus — only projects and folders can be focused. Attempting to focus a task or tag raises an error.
- Perspectives — perspective names are accessed via
perspective names (plural), switching via set perspective name of window to X.
- Move operations —
move task to before/after referenceTask requires both tasks to be in the same project and at the same hierarchy level.