14 KiB
Runtime API Query Contract
This document defines the ideal end-state for TaskNotes runtime query APIs used by companion plugins such as TaskNotes Workflows.
Context
TaskNotes now exposes an in-process JavaScript runtime API for companion plugins. Recent runtime API work added namespaced task, query, catalog, event, lifecycle, and extension surfaces.
That direction is correct. The remaining risk is that the current query contract exposes FilterQuery directly. FilterQuery is powerful, but it was shaped for TaskNotes UI state: every node has an id, it carries view grouping fields, and it uses UI-oriented operator names. Companion plugins need a stable task-selection contract, not a leak of current view/filter internals.
The ideal end-state is:
- TaskNotes core owns task query semantics.
- Companion plugins call TaskNotes for task selection instead of reimplementing filters.
- Workflow engines remain orchestration layers, not query engines.
- The public query DTO is stable, serializable, validation-friendly, and independent of current UI state.
- Bases-compatible syntax can be imported or compiled later, but the runtime API has its own canonical structured model.
Goals
- Provide one public task-selection query format for runtime API, HTTP API, MCP, CLI, and companion plugins.
- Keep task query semantics consistent with TaskNotes views where appropriate.
- Make query validation and dry-run/explain output deterministic.
- Support user-defined fields, computed task fields, file fields, date comparisons, sorting, limits, and grouping without exposing UI-only state.
- Preserve a clear boundary between task selection and task mutation.
- Keep triggers cheap: workflow/event triggers may filter event payloads, but vault-wide selection belongs in explicit query calls.
Non-Goals
- Do not expose arbitrary JavaScript, Dataview, or unrestricted Bases formulas as the runtime query contract.
- Do not make the runtime API depend on Obsidian Bases as the query evaluator.
- Do not require companion plugins to know TaskNotes frontmatter mapping details.
- Do not make every workflow step a hidden query step.
- Do not treat the current
FilterQueryshape as the long-term public contract unless it is intentionally cleaned up first.
Current State
Current runtime APIs include:
api.tasks.list(query?: FilterQuery): Promise<TaskInfo[]>api.query.tasks(query?: FilterQuery): Promise<TaskNotesRuntimeTaskQueryResult>api.query.filterOptions(): Promise<FilterOptions>api.catalog.filterProperties(): TaskNotesRuntimeFilterPropertyDefinition[]api.catalog.filterOperators(): TaskNotesRuntimeFilterOperatorDefinition[]
Current strengths:
- Query execution delegates to
FilterService. - Filter evaluation already handles nested groups, user fields, project links, computed dependency fields, and completion state.
- Catalog metadata gives companion-plugin editors a way to avoid hardcoded status, priority, field, and operator lists.
- Mutation APIs route through TaskNotes services and include mutation context for debuggability.
Current public-contract concerns:
FilterCondition.idandFilterGroup.idare UI state, not query semantics.FilterQuerymixes selection with view grouping state.FilterQueryhas no first-classlimit,offset, or paging contract.tasks.list(query?)andquery.tasks(query?)overlap without a sharply documented difference.- Query result counts do not distinguish total, matched-before-limit, and returned-after-limit.
- Operators are UI phrase IDs such as
is-on-or-before; that may be acceptable, but should be part of an intentional stable vocabulary. - There is no
normalize,validate, orexplainAPI for companion plugins to test a query before running an automation.
Proposed Public Query DTO
Introduce a stable public query shape, separate from internal FilterQuery.
export interface TaskNotesRuntimeTaskQuery {
where?: TaskNotesRuntimePredicate;
sort?: TaskNotesRuntimeSort[];
limit?: number;
offset?: number;
group?: TaskNotesRuntimeGroup[];
scope?: TaskNotesRuntimeQueryScope;
}
export type TaskNotesRuntimePredicate =
| { all: TaskNotesRuntimePredicate[] }
| { any: TaskNotesRuntimePredicate[] }
| { not: TaskNotesRuntimePredicate }
| TaskNotesRuntimeCondition;
export interface TaskNotesRuntimeCondition {
field: string;
op: TaskNotesRuntimeOperator;
value?: TaskNotesRuntimeValue;
}
export type TaskNotesRuntimeValue =
| string
| number
| boolean
| null
| TaskNotesRuntimeValue[]
| { fn: "today" | "now" }
| { fn: "date"; value: string }
| { fn: "dateAdd"; value: TaskNotesRuntimeValue; amount: number; unit: "day" | "week" | "month" };
export interface TaskNotesRuntimeSort {
field: string;
direction?: "asc" | "desc";
}
export interface TaskNotesRuntimeGroup {
field: string;
}
export interface TaskNotesRuntimeQueryScope {
includeArchived?: boolean;
folders?: string[];
excludeFolders?: string[];
}
The query engine should normalize this DTO into the internal representation used by FilterService.
Field Identity
Runtime query fields should be stable semantic IDs, not raw frontmatter keys.
Recommended canonical namespaces:
task.titletask.statustask.prioritytask.duetask.scheduledtask.completedDatetask.archivedtask.tagstask.contextstask.projectstask.recurrencetask.timeEstimatetask.totalTrackedTimetask.hasSubtaskstask.isBlockedtask.isBlockingtask.blockedByfile.pathfile.namefile.folderfile.tagsfile.ctimefile.mtimeuser.<id-or-key>
TaskNotes can support aliases for compatibility and authoring ergonomics:
- Current
FilterPropertyIDs such asstatus,due,dependencies.isBlocked. - Bases-like aliases such as
note.statusandnote.due.
Aliases should compile into canonical field IDs before validation or execution. Run logs and explain output should display the normalized canonical query.
Operators
Use a compact canonical operator vocabulary while accepting legacy aliases.
Recommended canonical operators:
eqnecontainsnotContainsinnotInexistsmissingltltegtgteisTrueisFalse
Compatibility aliases may map from current UI operator IDs:
is->eqis-not->nedoes-not-contain->notContainsis-before->ltis-on-or-before->lteis-after->gtis-on-or-after->gteis-empty->missingis-not-empty->existsis-checked->isTrueis-not-checked->isFalse
The catalog should expose both:
- canonical operator IDs for API clients
- display labels for UI clients
- accepted aliases for migration and authoring
- operator compatibility by field value type
Query Result
Replace or augment the current result with explicit count semantics.
export interface TaskNotesRuntimeTaskQueryResult {
tasks: TaskInfo[];
total: number; // total tasks in scope before where
matched: number; // tasks matching where before offset/limit
returned: number; // tasks returned after offset/limit
groups?: TaskNotesRuntimeQueryGroup[];
query: TaskNotesRuntimeNormalizedTaskQuery;
warnings?: TaskNotesRuntimeQueryWarning[];
}
export interface TaskNotesRuntimeQueryGroup {
key: string;
label: string;
taskPaths: string[];
}
If grouping is not requested, omit groups or return a single all group. Do not make grouping mandatory for simple selection.
Validation, Normalization, And Explain
Add explicit query support helpers:
export interface TaskNotesRuntimeQueryApi {
tasks(query?: TaskNotesRuntimeTaskQuery): Promise<TaskNotesRuntimeTaskQueryResult>;
validate(query: unknown): TaskNotesRuntimeQueryValidationResult;
normalize(query: unknown): TaskNotesRuntimeNormalizedTaskQuery;
explain(query: unknown): Promise<TaskNotesRuntimeQueryExplainResult>;
filterOptions(): Promise<FilterOptions>;
}
validate should report unsupported fields, unsupported operators, wrong value types, and invalid helper functions without executing the query.
normalize should:
- expand aliases
- fill defaults
- strip UI-only fields
- canonicalize operators
- resolve field IDs to catalog entries
explain should return enough data for dry runs and workflow debugging:
- normalized query
- candidate count
- matched count
- returned count
- applied sort
- applied limit/offset
- optimization notes where useful
- validation warnings
Relationship To Existing FilterQuery
Keep FilterQuery as an internal or legacy DTO unless it is deliberately promoted.
Recommended migration path:
- Add
TaskNotesRuntimeTaskQueryalongside existingFilterQuery. - Add an adapter from runtime query DTO to internal
FilterQuery. - Keep
api.tasks.list(query?: FilterQuery)working for compatibility during the transition. - Prefer
api.query.tasks(query?: TaskNotesRuntimeTaskQuery)in docs and companion-plugin examples. - Optionally allow
api.query.tasks()to accept legacyFilterQueryduring a deprecation window, but normalize it immediately. - Move query contract types toward a standalone runtime API package when the shape stabilizes.
The adapter should be the only place where public query fields map to internal filter properties, field mapping, user fields, and current FilterService operator names.
Catalog Requirements
The runtime catalog should support editor generation and query validation.
Each queryable field should include:
- canonical field ID
- display label
- category
- value type
- source:
model,computed,file, oruser - writable flag
- queryable flag
- sortable flag
- groupable flag
- supported canonical operators
- accepted aliases
- frontmatter key, where applicable
- deprecation metadata, where applicable
Each operator should include:
- canonical ID
- display label
- value required flag
- accepted value types
- accepted aliases
Workflows Integration
TaskNotes Workflows should use TaskNotes runtime query APIs for task selection.
Workflow usage should look like this:
steps:
- id: due-now
type: task.query
input:
query:
where:
all:
- field: task.status
op: notIn
value: [done, cancelled]
- field: task.due
op: lte
value:
fn: today
sort:
- field: task.due
direction: asc
limit: 50
- id: mark-high
type: task.patch
forEach: "{{steps.due-now.tasks}}"
input:
task: "{{item.path}}"
patch:
priority: high
Workflow-level conditions and step-level if may use a similar predicate model, but they evaluate against workflow context, not the TaskNotes task index. They should not call api.query.tasks() implicitly.
Trigger filters should stay event-local. A trigger can inspect event, before, after, changes, source, and path, but vault-wide queries should run as explicit steps after the workflow starts.
Bases Compatibility
Bases is the right mental-model inspiration, not the runtime dependency.
TaskNotes should eventually be able to:
- compile a subset of Bases filter expressions into
TaskNotesRuntimeTaskQuery - import filters from a
.basefile where the mapping is unambiguous - expose familiar aliases such as
note.status,note.due, andfile.path
TaskNotes should not:
- depend on Obsidian Bases as the query evaluator
- claim full Bases formula compatibility unless it actually matches edge cases
- expose arbitrary formula strings as the canonical runtime query contract
If string syntax is added, it should compile into the structured query DTO. The structured form remains canonical.
API Naming Decisions
Recommended naming:
api.query.tasks(query)is the canonical selection API.api.tasks.list()is a simple unfiltered list or a compatibility alias.api.stats.tasks(query)accepts the same canonical query.api.catalog.queryFields()may be clearer than overloadingcatalog.filterProperties(), but either is acceptable if documented.
Avoid making companion plugins choose between multiple task-selection APIs with subtly different semantics.
Testing Requirements
Add unit coverage for:
- normalization from canonical query DTO to internal
FilterQuery - legacy
FilterQuerycompatibility, if supported - field aliases:
task.status,note.status,status - operator aliases:
lte,is-on-or-before, etc. - user fields by ID and key
- computed fields:
task.hasSubtasks,task.isBlocked,task.isBlocking,task.status.isCompleted - date helpers:
today,now, and simple date offsets - limit/offset counts
- invalid fields and invalid operator/value combinations
explainoutputapi.stats.tasks(query)matchingapi.query.tasks(query)selection semantics
Add integration/smoke coverage for:
- a companion-plugin style query through
api.query.tasks - a workflow-style query selecting overdue incomplete tasks
- a query using user fields from settings
- a query involving dependency/computed fields
Implementation Prompt
Use this prompt for an implementation pass:
We are hardening the TaskNotes JavaScript runtime API for companion plugins. Inspect the current runtime API in src/api/runtime-api.ts and src/api/TaskNotesAPI.ts, the current FilterQuery types in src/types.ts, and FilterService/filterPredicateEvaluation. Implement a stable public task-selection query DTO that does not leak UI-only FilterQuery state. Keep TaskNotes core as the owner of task query semantics.
Target outcome:
- Add TaskNotesRuntimeTaskQuery, TaskNotesRuntimePredicate, canonical field/operator types, normalization, validation, and explain support.
- Keep existing FilterQuery working internally and through a compatibility path where practical.
- Make api.query.tasks(query) the canonical public selection API.
- Keep api.tasks.list() as simple list/compatibility, but document the distinction.
- Expose catalog metadata sufficient for companion-plugin query editors.
- Preserve current TaskNotes filtering behavior by translating the public query DTO into the existing FilterService pipeline.
- Do not introduce arbitrary JavaScript, Dataview, or a dependency on Obsidian Bases as the evaluator.
- Update docs/javascript-api.md and focused unit tests.
Important boundaries:
- Task mutations must continue through TaskService-backed runtime APIs.
- Workflow/event triggers should only filter event payloads; vault-wide task selection belongs in explicit query calls.
- The normalized query should be visible in query result or explain output so dry runs can be debugged.