TaskNotes Workflows is a companion plugin for Markdown-defined automation. It lets you create workflow notes that respond to TaskNotes events, schedules, manual commands, and selected Obsidian events, then run typed actions against TaskNotes tasks.
TaskNotes Workflows keeps this behavior outside TaskNotes core while still using the [TaskNotes JavaScript Runtime API](../javascript-api.md) for safe task reads, writes, canonical queries, events, and relationship resolution.
- TaskNotes must expose the JavaScript runtime API.
- Obsidian must be open for workflows to run.
- Scheduled workflows run while Obsidian is running. They are not a background service when Obsidian is closed.
- The plugin runtime uses Obsidian APIs only. Node is used for development and build scripts, not for workflow execution, so Markdown workflow definitions can work on Obsidian mobile while the plugin is loaded.
| `TaskNotes/Views/workflows.base` | Generated Obsidian Base that renders workflow cards through the custom workflow view. |
| Plugin config folder | Run history and detail logs, unless a run-log folder is configured. |
Workflow notes are ordinary Markdown files. The YAML frontmatter is executable configuration, and the note body is for human explanation, testing notes, and maintenance guidance.
## First Run
On first load, the plugin can create a default workflow folder, a workflow Base, and a set of example workflow notes. The default workflow notes are disabled so they can be inspected, dry-run, edited, and enabled intentionally.
The generated Base is the main workbench. It shows workflow cards with status, trigger and step summaries, recent run state, and buttons for editing, dry-running, running, and opening the source note.
Workflow notes also show a compact workflow card in reading mode and live preview, so the note remains useful even when opened directly.
## Default Workflows
The plugin can write these starter workflows:
| Workflow | What it demonstrates |
| --- | --- |
| Auto-start time tracking | Starts a timer when a task status changes to active. |
| Stop time tracking on complete | Stops the timer when a task is completed. |
| Morning overdue review | Runs a daily cron query and marks overdue open tasks high priority. |
| Move completed tasks to review | Moves completed task notes into a review folder. |
| Inherit subtask contexts and tags | Copies contexts and tags from the first parent task when a task becomes a subtask. |
| Inherit subtask priority | Copies priority from the first parent task when a task becomes a subtask. |
| Inherit subtask planning dates | Copies scheduled and due dates from the first parent task when a task becomes a subtask. |
| Inherit subtask dependencies | Adds the first parent task's blocking dependencies to new subtasks. |
| Mirror parent priority to subtasks | Updates existing subtasks when a parent task priority changes. |
| Mirror parent planning dates to subtasks | Updates existing subtasks when a parent scheduled or due date changes. |
| Mirror parent dependencies to subtasks | Replaces each subtask's dependencies with the parent task's current dependencies. |
| Warn when starting a blocked task | Shows a notice when a task becomes active while dependencies remain. |
| Daily active task review | Shows a daily count of tasks still marked active. |
Treat these as editable examples. Adjust folders, statuses, priorities, dates, and filters to match your vault before enabling mutating workflows.
## Workflow Shape
A workflow file looks like this:
```markdown
---
type: tasknotes-workflow
schemaVersion: 1
id: auto-start-time-tracking
name: Auto-start time tracking
enabled: false
description: Start a timer when a task status changes to active.
triggers:
- id: status-active
type: tasknotes.event
event: task.status.changed
to: active
conditions:
- field: trigger.after.path
operator: exists
steps:
- id: start-time
type: time.start
input:
task: "{{trigger.after.path}}"
options:
description: "Started by {{workflow.name}}"
run:
mode: sequential
noOverlap: true
source: tasknotes-workflows
maxTasks: 1
onError: stop
---
# Auto-start time tracking
Enable this workflow to start time tracking when a TaskNotes task moves to `active`.
```
Required frontmatter fields:
| Field | Type | Notes |
| --- | --- | --- |
| `type` | string | Must be `tasknotes-workflow`. |
| `schemaVersion` | number | Must be `1`. |
| `id` | string | Stable lowercase identifier. Use letters, numbers, dots, underscores, and dashes. |
| `name` | string | Human-readable name shown in workflow cards and command-palette commands. |
| `enabled` | boolean | Disabled workflows can be edited and dry-run but do not run automatically or from manual commands. |
| `triggers` | array | One or more trigger definitions. |
| `run` | object | Run policy and safety limits. |
## Triggers
Triggers decide when a workflow should run.
### TaskNotes Events
Use `tasknotes.event` to react to TaskNotes runtime events:
```yaml
triggers:
- id: status-active
type: tasknotes.event
event: task.status.changed
from: open
to: active
```
Common event names include:
-`task.created`
-`task.updated`
-`task.deleted`
-`task.moved`
-`task.status.changed`
-`task.completed`
-`task.uncompleted`
-`task.archived`
-`task.unarchived`
-`task.scheduled.changed`
-`task.due.changed`
-`task.priority.changed`
-`task.tags.changed`
-`task.contexts.changed`
-`task.projects.changed`
-`task.reminders.changed`
-`task.dependencies.changed`
-`task.recurrence.changed`
-`time.started`
-`time.stopped`
-`pomodoro.started`
-`pomodoro.completed`
-`pomodoro.interrupted`
-`recurring.instance.completed`
-`recurring.instance.skipped`
The workflow editor loads available TaskNotes events dynamically from `api.events.list()`, so event pickers can follow the runtime API instead of relying on a hardcoded list.
TaskNotes event triggers can use these filters:
| Field | Purpose |
| --- | --- |
| `event` | Runtime event name. |
| `from` | Previous status or value for status-change workflows. |
| `to` | New status or value for status-change workflows. |
| `path.glob` | Optional path glob limiting which task files can trigger the workflow. |
| `allowSelfTrigger` | Allows this workflow to react to writes caused by `tasknotes-workflows`. Leave off unless you have a clear reason. |
TaskNotes event output fields include values such as `trigger.after.path`, `trigger.after.status`, `trigger.before.status`, `trigger.changes`, `trigger.source`, and `trigger.correlationId`.
### Cron
Cron triggers run on matching minutes while Obsidian is open:
```yaml
triggers:
- id: weekday-review
type: cron
schedule: "0 17 * * 1-5"
timezone: local
```
The schedule uses five fields: minute, hour, day of month, month, day of week. Keep cron workflows bounded with `run.maxTasks`, especially if they query many tasks.
### Interval
Interval triggers run repeatedly while Obsidian is open:
```yaml
triggers:
- id: half-hour-check
type: interval
every: 30m
```
The plugin enforces a minimum interval to avoid excessive background work.
### Manual
Manual triggers let a workflow run from a card button, another plugin, or the Obsidian command palette:
```yaml
triggers:
- id: manual
type: manual
```
Enabled workflows with a manual trigger are registered as Obsidian commands named `Run: <workflow name>`. You can launch them from the command palette or assign hotkeys in Obsidian settings.
The generated command id is based on the workflow `id`. Changing a workflow id creates a different command, so any hotkey assigned to the old command id will need to be reassigned.
### Obsidian Events
Advanced Obsidian triggers are opt-in from the plugin settings. They can listen to selected vault, metadata, and workspace events:
```yaml
triggers:
- id: project-note-opened
type: obsidian.workspace
event: file-open
path:
glob: "Projects/**/*.md"
- id: task-note-modified
type: obsidian.vault
event: modify
path:
glob: "Tasks/**/*.md"
- id: metadata-changed
type: obsidian.metadata
event: changed
path:
glob: "Projects/**/*.md"
```
Keep path filters narrow for Obsidian triggers. Vault and metadata events can be frequent.
## Conditions
Conditions can appear at workflow level or on individual steps.
`task.query` uses the canonical TaskNotes runtime query DTO. Its output includes `tasks`, `count`, `total`, `matched`, `returned`, `groups`, `groupPaths`, `query`, and `warnings`.
Runtime query conditions use canonical field IDs and operators:
```yaml
query:
where:
all:
- field: task.status
op: ne
value: done
- any:
- field: task.due
op: lte
value:
fn: today
- field: task.priority
op: eq
value: high
sort:
- field: task.due
direction: asc
group:
- field: task.status
limit: 25
scope:
includeArchived: false
```
Common fields include `task.status`, `task.priority`, `task.due`, `task.scheduled`, `task.projects`, `task.contexts`, `task.tags`, `task.isBlocked`, and `file.path`. Operators include `eq`, `ne`, `contains`, `notContains`, `in`, `notIn`, `exists`, `missing`, `lt`, `lte`, `gt`, `gte`, `isTrue`, and `isFalse`. Use `api.catalog.filterProperties()` and `api.catalog.filterOperators()` for the complete runtime catalog.
| `mode` | Currently `sequential`. Steps run in order. |
| `noOverlap` | Skips a new run if the same workflow is already running. |
| `source` | Mutation source passed to TaskNotes runtime API calls. |
| `maxTasks` | Safety limit for `forEach` batch steps. |
| `onError` | `stop` or `continue`. |
| `timeout` | Optional duration string. |
TaskNotes attaches `source`, correlation, and reason metadata to events caused by runtime API writes. Workflows use this to make runs debuggable and to avoid accidental self-trigger loops.
## Run Logs and Debugging
Workflow run logs are stored under the plugin's Obsidian config folder by default. You can configure a run-log folder and a log level in the plugin settings.
Run records include:
- Workflow id, name, and source path.
- Trigger payload.
- Step statuses.
- Inputs and outputs, depending on the configured log level.
- Error text when a run fails.
Use dry runs before enabling workflows that mutate tasks. Dry runs still execute the workflow path, but mutating steps should report what they would do instead of changing task files.
## Example: Manual Command Workflow
This workflow can be run from the command palette after it is enabled:
```markdown
---
type: tasknotes-workflow
schemaVersion: 1
id: show-active-count
name: Show active task count
enabled: true
description: Show a notice with the number of active tasks.
Run this from the Obsidian command palette with `TaskNotes Workflows: Run: Show active task count`.
```
## Example: Inherit Parent Priority
This workflow copies priority from the first parent task when a task becomes a subtask:
```yaml
type: tasknotes-workflow
schemaVersion: 1
id: inherit-subtask-priority
name: Inherit subtask priority
enabled: false
triggers:
- id: projects-changed
type: tasknotes.event
event: task.projects.changed
- id: task-created
type: tasknotes.event
event: task.created
conditions:
- field: trigger.after.path
operator: exists
- field: trigger.after.projects
operator: exists
steps:
- id: parents
type: task.parents
input:
task: "{{trigger.after.path}}"
- id: inherit-priority
type: task.patch
if:
- field: steps.parents.tasks[0].priority
operator: exists
input:
task: "{{trigger.after.path}}"
patch:
priority: "{{steps.parents.tasks[0].priority}}"
run:
mode: sequential
noOverlap: true
source: tasknotes-workflows
maxTasks: 1
onError: stop
```
This pattern uses `task.parents` rather than trying to parse project links manually.
## Authoring Guidelines
When writing workflow notes:
- Start with `enabled: false` for any workflow that mutates tasks.
- Give every trigger and step a stable `id`.
- Prefer typed TaskNotes steps over direct frontmatter editing.
- Use relationship read steps such as `task.parents`, `task.subtasks`, and `task.dependencies` for parent/subtask/dependency logic.
- Add workflow-level conditions for required trigger fields such as `trigger.after.path`.
- Add `run.noOverlap: true` unless overlapping runs are intentional.
- Set `run.maxTasks` for batch workflows.
- Use `allowSelfTrigger: true` only when the workflow really should react to its own writes.
- Keep the Markdown body useful for future maintenance: describe what the workflow changes, how to test it, and why it exists.
## Troubleshooting
### Task steps do not run
Confirm TaskNotes is enabled and exposes the runtime API. The workflow Base can display definitions without TaskNotes, but steps such as `task.patch`, `time.start`, or `task.dependencies` need TaskNotes.
### A manual workflow is missing from the command palette
Check that:
- The workflow is valid.
-`enabled` is `true`.
- It has a `manual` trigger.
- The workflow `id` is valid and stable.
- Workflows have reloaded after editing the file.
Disabled manual workflows can still be dry-run from the workflow card, but they are not registered as run commands.
### A cron or interval workflow did not run
Scheduled workflows only run while Obsidian is open and the plugin is loaded. Check the schedule, timezone, enabled state, and `run.maxTasks` limit.
### A workflow runs more than once
Check whether multiple triggers match the same update. Also check `allowSelfTrigger`; most workflows should leave it unset so they do not react to writes caused by `tasknotes-workflows`.
### A workflow is invalid
Open the workflow Base or the workflow note card. Validation messages point to the field path, such as `triggers[0].type` or `steps[1].input`.
Common causes are missing required fields, unsupported trigger types, invalid YAML, or references to step outputs that do not exist.