Accessing Omnifocus taskStatus (available etc.) using JXA

I’d like to retrieve “available” tasks from a project using JXA. I’m using this to output the top 5 tasks I have for a day into an Obsidian daily note, for what it’s worth.

I can get a list of incomplete tasks using the following JXA script:

function run(args) {
    const projectName = "Now Chores"
    const ofApp = Application("OmniFocus")
    const ofDoc = ofApp.defaultDocument

    const project = ofDoc.flattenedProjects
        .whose({ name: projectName })[0];

    const tasks = project.tasks.whose({ completed: false })()
        .map((task) => {
            return `- [ ] ${task.name()}`;
        })
	
	return JSON.stringify(tasks)
}

However, this shows all incomplete tasks, including ones that are not yet available. Now, the task object returned by whose has a taskStatus attribute of type Task.Status. From there, one can get the Available status.

However, all the examples I can find are not using JXA but instead using JS from within Omnifocus. When executing within Omnifocus, one can do task.taskStatus == Task.Status.Available as the Task type is inserted into the JS global scope.

With JXA Task isn’t available in the global scope, and indeed, task.taskStatus() (where the () is supposed to “unwrap” into JS types) gives a type error; from Script Editor, if you try to call taskStatus() within the map function above:

	app.defaultDocument.tasks.byId("dupLN0rlW3s").taskStatus()
		--> Error -1700: Can't convert types.

What I’m looking for is either a block for filter within JavaScript, an argument for whose or something else that lets me filter by the richer task status.

Anyone any ideas? Thanks!

If you look at OmniFocus scripting dictionary, you can see there isn’t a taskStatus property.

Task Class
Task Object : A task. This might represent the root of a project, an action within a project or other action or an inbox item.
elements
contains tags, tasks, flattenedTasks; contained by documents, tags, tasks.
properties
id (text) : The identifier of the task.
name (text) : The name of the task.
note (RichText) : The note of the task.
container (Document, QuickEntryTree, Project, or Task, r/o) : The containing task, project or document.
containingProject (Project or missing value, r/o) : The task's project, up however many levels of parent tasks. Inbox tasks aren't considered contained by their provisionalliy assigned container, so if the task is actually an inbox task, this will be missing value.
parentTask (Task or missing value, r/o) : The task holding this task. If this is missing value, then this is a top level task -- either the root of a project or an inbox item.
containingDocument (Document or QuickEntryTree, r/o) : The containing document or quick entry tree of the object.
inInbox (boolean, r/o) : Returns true if the task itself is an inbox task or if the task is contained by an inbox task.
primaryTag (Tag or missing value) : The task's first tag. Setting this will remove the current first tag on the task, if any and move or add the new tag as the first tag on the task. Setting this to missing value will remove the current first tag and leave any other remaining tags.
completedByChildren (boolean) : If true, complete when children are completed.
sequential (boolean) : If true, any children are sequentially dependent.
flagged (boolean) : True if flagged
next (boolean, r/o) : If the task is the next task of its containing project, next is true.
blocked (boolean, r/o) : True if the task has a task that must be completed prior to it being actionable.
creationDate (date) : When the task was created. This can only be set when the object is still in the inserted state. For objects created in the document, it can be passed with the creation properties. For objects in a quick entry tree, it can be set until the quick entry panel is saved.
modificationDate (date, r/o) : When the task was last modified.
deferDate (date or missing value) : When the task should become available for action.  syn startDate
effectiveDeferDate (date or missing value, r/o) : When the task should become available for action (including inherited).
dueDate (date or missing value) : When the task must be finished.
effectiveDueDate (date or missing value, r/o) : When the task must be finished (including inherited).
shouldUseFloatingTimeZone (boolean) : When set, the due date and defer date properties will use floating time zones. (Note: if a Task has no due or defer dates assigned, this property will revert to the database’s default setting.)
completionDate (date or missing value) : The task's date of completion. This can only be modified on a completed task to backdate the completion date.
completed (boolean, r/o) : True if the task is completed. Use the "mark complete" and "mark incomplete" commands to change a task's status.
effectivelyCompleted (boolean, r/o) : True if the task is completed, or any of it's containing tasks or project are completed.
droppedDate (date or missing value) : The date the task was dropped. This can only be modified on a dropped task to backdate the dropped date.
dropped (boolean, r/o) : True if the task is dropped. Use the "mark dropped" and "mark incomplete" commands to change a task's status.
effectivelyDropped (boolean, r/o) : True if the task is dropped, or any of it's containing tasks or project are dropped.
estimatedMinutes (integer or missing value) : The estimated time, in whole minutes, that this task will take to finish.
repetitionRule (RepetitionRule or missing value) : The repetition rule for this task, or missing value if it does not repeat.
nextDeferDate (date or missing value, r/o) : The next defer date if this task repeats on a fixed schedule and it has a defer date.
nextDueDate (date or missing value, r/o) : The next due date if this task repeats on a fixed schedule and it has a due date.
numberOfTasks (integer, r/o) : The number of direct children of this task.
numberOfAvailableTasks (integer, r/o) : The number of available direct children of this task.
numberOfCompletedTasks (integer, r/o) : The number of completed direct children of this task.

You can either:

  • use JXA and chain the set of conditions that define a task as available, or
  • just use OmniFocus Omni-Automation API, which has a taskStatus property (Task class)

I would choose the latter options, as it is cross-platform compatible.

FWIW, assuming a project named “Now Chores”, can you fill the dots in order to return available (incomplete) tasks ?

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = () => {
        // main :: IO ()
        const main = () => {
            const
                proj = flattenedProjects.byName("Now Chores");
            return proj.tasks.filter(
                x => true // ...
            )
        }

        return main();
    };


    // OmniJS Context Evaluation ------------------------------------------------
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})()`
        )
    ) : 'No documents open in OmniFocus.'
})();

Feel free to write here if you have further questions.

1 Like

Ah! I’d not realised that the scripting dictionary different from the OmniAutomation functionality. That clears up a bunch. Thank you!


As for the answer for the “pure JXA approach”, from the results that I get, it looks like you can use blocked: false, completed: false as a surrogate for “available”, as in:

const tasks = project.tasks.whose({ blocked: false, completed: false })()
    .map((task) => {
        return `- [ ] ${task.name()}`;
    })

I’m not sure whether this covers the whole logic of “available” in OF itself, but it’s a start.

Frustratingly, tags have an availableTasks property, but projects do not (and it looks like this request isn’t new, e.g., this thread from 2015 Detect whether a task is available).

The scripting dictionary has an AvailableTask type, but I’ve no idea how to use it (for example, from the tasks attribute of a Project) from the somewhat cryptic (to me):

A task that is available for action. This is simply a filter on the existing tasks and should be considred a read-only element.


Using the evaluateJavascript approach, which I’d not realised allowed you to receive JXA-usable task objects, you can access the Task.Status.Available from within the OmniJS context:

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = () => {
        // main :: IO ()
        const main = () => {
            const proj = flattenedProjects.byName("Now Chores");
            return proj.tasks.filter(
                x => x.taskStatus === Task.Status.Available
            )
        }

        return main();
    };


    // OmniJS Context Evaluation -----------------------------
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})()`
        ).map(x => x.name())
    ) : 'No documents open in OmniFocus.'
})();

You’re welcome !

An incomplete and unblocked task could be deferred to 1st March, for example. It would pass your criteria, but it isn’t “available”.

If you access availableTasks property of a tag, you should get an array of elements. Each one of those belong to AvailableTask class. As you say, only tags have this property.

There is no JXA way to directly access available tasks of a certain project, since that property isn’t available for that kind of object.

What do you mean by that ? Omni-Automation Task class doesn’t have anything to do with OmniFocus JXA Task class, but perhaps I’m missing your point.

Well done :-)

1 Like

An incomplete and unblocked task could be deferred to 1st March, for example. It would pass your criteria, but it isn’t “available”.

I thought that too, but adding blocked: true to the whose call also excludes deferred tasks – my “chores” project has lots of deferred recurring tasks, and these are all excluded from the results.


What do you mean by that ? Omni-Automation Task class doesn’t have anything to do with OmniFocus JXA Task class, but perhaps I’m missing your point.

When I say that the return value from executeJavascript is usable from JXA, I mean that the call to executeJavascript returns a list of objects that I’m able to use from JXA right away, rather than something I need to convert first (you can see this works as I’m able to use .map(x => x.name()) tacked onto the executeJavascript call).

Based on the “replies” pane in Script Editor, the array returned by Omnifocus to the script looks like this:

[app.defaultDocument.tasks.byId("on5t4qrLwPu"), ... more tasks ...]

A whose call returns the same array of app.defaultDocument.tasks.byId type items.

So the executeJavascript call appears to somehow figure out to return something that I can use in the AppleScript/JXA type environment, which is useful!


One further question I have about executeJavascript is whether one can pass in parameters without resorting to string templating. The only way I’ve gotten it working so far is to use string templating to insert the variable’s value into the executeJavascript as follows, I’m wondering whether there’s a smarter way (I’ve slightly simplified the omniJSContext code to make it clearer what’s happening, not sure if removing the indirection via main matter or not):

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = (project) => {
        const proj = flattenedProjects.byName(project);
        return proj.tasks.filter(
            x => x.taskStatus === Task.Status.Available
        )
    };


    // OmniJS Context Evaluation -----------------------------
	
	const project = "Now Chores" // <- can one get this into omniJSContext?
	
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})("${project}")`
        ).map(x => x.name())
    ) : 'No documents open in OmniFocus.'
})();

Good ! I am going to start an evaluation in a couple of minutes. I’ll properly respond to your messages when I finish.

Perhaps, that is a bug. The dictionary says:

blocked (boolean, r/o) : True if the task has a task that must be completed prior to it being actionable.

That’s an interesting discovery. However, if I select the first item and call its name property in the JXA context:

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = (project) => {
        const proj = flattenedProjects.byName(project);
        return proj.tasks.filter(
            x => x.taskStatus === Task.Status.Available
        )[0]
    };


    // OmniJS Context Evaluation -----------------------------
	
	const project = "Now Chores" // <- can one get this into omniJSContext?
	
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})("${project}")`
        ).name()
    ) : 'No documents open in OmniFocus.'
})();

I get an execution error.

.evaluateJavascript evaluates the literal JS code passed as argument. What you are doing is fine and there is no better way, I think. That method doesn’t take any “extra arguments”; only one.

I am using it mainly for clarity. When I have multiple execution contexts, making a clear distinction between them helps, I think.

I get the same failure result for:

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = (project) => {
        const proj = flattenedProjects.byName(project);
        return proj.tasks.filter(
            x => x.taskStatus === Task.Status.Available
        )[0]
    };

It returns null rather than the item, which we can see by looking at the replies pane in the Script Editor. Here, we can see the invocation returns null, rather than the task item:

	app.evaluateJavascript("((project) => {\n        const pro...")
		--> null

(n.b., the replies pane actually lists the whole passed JS, I just truncated it for readability)

When returning the array rather than the single item, this instead reads:

	app.evaluateJavascript("((project) => {\n        const pro...")
		--> [app.defaultDocument.tasks.byId("on5t4frtwPu"), app.defaultDocument.tasks.byId("ddodp62XWe8")]

The difference in behaviour between the array and single task feels pretty odd, but I can understand why the testing here may have not been extensive given OmniJS -> JXA/AppleScript feels a bit niche.

I wouldn’t expect that kind of behavior for arrays or tasks. I am a bit surprised about your discovery since they are different interfaces. Not sure what happened behind the scenes in this implementation.

Have you checked that ?

That impression is sometimes just an artefact of stringification.

JSON.stringify shows the string “null”, for example, in cases where an object is behind an interface to which it lacks access. (i.e. which it doesn’t know how to stringify).


(Try an === null test, for example)

1 Like

PS exercises in the use of evaluateJavascript are best designed to return strings (of your own explicit devising).

(Remember that (unlike primitive values – numbers, strings etc) OmniJS objects are not defined in the JXA name-space to which evaluateJavascript is returning a value, and it may be optimistic to expect it to be able to generate useful stringifications.)

1 Like

The null came from Script Editor’s “replies” pane, so I confirmed as suggested using an === null. With the rest of the code as above, this is true:

(
    Application('OmniFocus').evaluateJavascript(
        `(${omniJSContext})("${project}")`   
) === null

That makes sense. I guess that a decent general pattern, then, is using JSON to get things in and out, as follows:

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = (opts) => {
        opts = JSON.parse(opts)
        const proj = flattenedProjects.byName(opts.project);
        const tasks = proj.tasks.filter(
            x => x.taskStatus === Task.Status.Available
        )
        return JSON.stringify(tasks.map(x => x.name))
    };

    // OmniJS Context Evaluation -----------------------------
    const ofApp = Application('OmniFocus')
    const opts = { project: "Now Chores" }
    var result = []
    if (0 < ofApp.documents.length) {
        result = JSON.parse(ofApp.evaluateJavascript(
            `(${omniJSContext})('${JSON.stringify(opts)}')`
        ))
    }
    return result
})();

Remember that you have two quite different JS interpreters in play:

  • The omniJS JSContext
  • The JXA JSContext

Primitive values can be returned from omnJS into JXA, but not objects with a private component.

i.e. you need to be assembling your strings inside the the omniJS interpreter, and then returning those strings back to JXA and the Script Editor.

JSON.stringify (and any custom stringification) applied on the Script Editor (JXA) side is already too late,

and JSON.stringifyJSON.parse is not always fully reversible. (Try simple JS Date values for example)

evaluateJavascript can return standard JS types to JXA, but it can’t return types which are only defined in the omniJS interpreter.


Looking at this example from above:

// OMNI JS CODE ---------------------------------------
    const omniJSContext = (project) => {
        const proj = flattenedProjects.byName(project);
        return proj.tasks.filter(
            x => x.taskStatus === Task.Status.Available
        )[0]
    };

You can’t get a list of omniJS task objects (or a single task object) into the JXA interpreter.

evaluateJavascript can however, bring back a [String] list of task names, or the name of the single task of interest.

String objects are defined in JXA, but task objects are not – they are only defined, and stringifiable, in omniJS.

1 Like