supertag Automation System 2.0 is a modern, event-driven automation framework. It empowers every tag with powerful automation capabilities, allowing you to build truly intelligent, hands-free org-mode workflows.
- ✅ Unified Tag System: Every tag is a fully-featured "database" with custom fields and automation capabilities.
- ✅ True Event-Driven: Responds to precise data changes in real-time, rather than polling scans.
- ✅ Automatic Rule Indexing: Automatically builds high-performance indexes for rules in the background, without users needing to worry about performance optimization details.
- ✅ Multiple Action Execution: A single rule can trigger a series of sequentially executed actions.
- ✅ Scheduled Tasks: Supports time-based and periodic automation, driven by an integrated scheduler.
- ✅ Relationships and Calculations: Supports advanced features like bidirectional relationships, property synchronization, and Rollup calculations.
- ✅ Formula Fields: Calculates and displays data in real-time in table views without persistent storage.
- ✅ Legacy Interop: Some legacy storage/events are still supported where needed (details are called out explicitly below).
The new version adopts a unified module architecture:
- Unified API Surface:
supertag-automation.elis the public entry for rule CRUD + evaluation + actions. - Synchronous Event Bridge:
supertag-automation-sync.elroutes store/commit events into the rule engine. - Unified Interfaces: All user-facing functionality is accessed through a consistent rule shape.
- Index-Driven Lookup: Rules are pre-indexed to avoid scanning all rules on every event.
The core of the new architecture is simplicity and automation. We've eliminated the distinction between "regular tags" and "database tags," replacing the manual "behavior attachment" process with an intelligent "rule indexing" system.
| Feature | Old Version (1.0) | New Version (2.0) |
|---|---|---|
| Module Structure | Dispersed multiple modules with circular dependencies | Unified single module, eliminating dependency issues |
| Rule Management | Manually attached to tags, requiring user management | Automatic indexing, intelligently managed by the system |
| Performance | O(n) traversal of all rules | O(1) index lookup, high performance |
| API Consistency | Multiple different API interfaces | Unified API interface, low learning cost |
| Maintainability | Complex inter-module relationships | Simple cohesive design, easy to maintain |
;; New unified module loading approach
(require 'supertag-automation) ; Rule engine + public API
;; `supertag-automation-sync` is loaded internally as needed.
;; Optional (advanced): run automation via `supertag-after-operation-hook`.
;; Default is nil to avoid double-triggering (store emits multiple event streams).
;; (setq supertag-automation-sync-use-commit-hooks t)graph LR
subgraph "Old Architecture"
A1[Data Change] --> B1[Event Wakeup]
B1 --> C1[Traverse/Filter Rules]
C1 --> D1[Execute]
end
subgraph "New Architecture"
A2[Rule Definition] --> B2[Automatic Index Building in Background]
C2[Data Change] --> D2[Event Notification]
D2 --> E2[O(1) Index Query]
E2 --> F2[Precise Condition Evaluation]
F2 --> G2[Execute]
end
style B2 fill:#e8f5e8
style E2 fill:#e8f5e8
style G2 fill:#e1f5fe
Under the new system, you no longer need to "attach" behaviors. You simply define a rule, and the system automatically handles everything else.
(Note: All examples below assume you have pre-defined the required tags (such as #task, #project) and relationships using supertag-tag-create and supertag-relation-create. We focus here on creating the automation rules themselves.)
Scenario: When any node is tagged with #task, automatically set its TODO state to "TODO".
;; Create an automation rule
(supertag-automation-create
'(:name "auto-set-todo-on-task"
;; Trigger: When a node is tagged with "task"
:trigger (:on-tag-added "task")
;; Action list: Can contain one or more actions
:actions '((:action :update-todo-state
:params (:state "TODO")))))That's it!
Before Operation:
* A regular heading
Operation: In Org Mode, with cursor on the heading, run M-x supertag-add-tag and select/add task.
After Operation:
* TODO A regular heading #task
Effect Analysis: The rule is activated by the :on-tag-added trigger, executing the :update-todo-state action in the :actions list, automatically setting the headline TODO keyword.
Note:
supertagtags are represented as inline#tagtext and stored in the supertag database. UseM-x supertag-add-tag/M-x supertag-remove-tagto mutate tags;org-set-tags-commandedits Org's native:tag:mechanism and is not the same thing.
Relationships are one of supertag's core capabilities, responsible for establishing meaningful links between different types of data (defined by tags). For example, linking "project" notes with "task" notes.
You can define a relationship using the supertag-relation-create function.
When defining relationships, the most important property is :type, which determines how data is associated (cardinality).
| Type | Format | Description | Example |
|---|---|---|---|
| One-to-One | :one-to-one |
A "source" node can be associated with at most one "target" node. | A User can have only one Profile. A task can have at most one "predecessor task". |
| One-to-Many | :one-to-many |
A "source" node can be associated with multiple "target" nodes, but each "target" node can only be associated with one "source" node. | A #Project can contain multiple #Tasks. A #Notebook can contain multiple #Notes. |
| Many-to-Many | :many-to-many |
"Source" and "target" nodes can be arbitrarily associated with each other, with no limit on quantity. | An #Article can have multiple #Keywords; a #Keyword can also be used in multiple #Articles. |
Rollup is a powerful feature of the relationship system that allows "one" end nodes (such as #Project) to automatically collect data from all associated "many" end nodes (such as multiple #Tasks) and perform real-time calculations.
You can configure rollup by adding a :rollup property when defining :one-to-many or :many-to-many relationships.
The :rollup property itself is a property list (plist) containing the following three key parameters:
| Parameter | Description | Example |
|---|---|---|
:from-field |
Specifies which property to collect data from on the "many" end nodes (source). | Collect :hours property values from all #Task nodes. |
:to-field |
Specifies which property on the "one" end node (target) to write the calculation result to. | Write the calculation result to the :total_hours property of the #Project node. |
:function |
Specifies which function to use to process the collected data. | Use the sum function to add up all the hours. |
The system has built-in several commonly used calculation functions:
| Function Name | Description | Test Status |
|---|---|---|
sum |
Calculate the sum of all numeric values. | ✅ Verified |
count |
Count the total number of associated nodes. | ✅ Verified |
average |
Calculate the average of all numeric values. | ✅ Verified |
min / max |
Find the minimum or maximum value among all numeric values. | ✅ Verified |
unique-count |
Count how many unique property values there are. | ✅ Verified |
concat |
Concatenate all property values (usually text) into a string. | ✅ Verified |
first / last |
Return the first or last value. | ✅ Supported |
Planned Features (Future Versions):
| count-where-filled | Count nodes with non-empty values. | 🔄 Planned |
| percent-done | Calculate completion percentage. | 🔄 Planned |
Formula fields are an innovative feature of supertag that allows you to define "virtual columns" in table views, whose values are calculated in real-time based on other fields. The calculation results of formula fields are not stored in node properties; they are only calculated and displayed when the table view is rendered.
In tag definitions, you can declare formula fields just like regular fields, but their :type is :formula, and they include a :formula property to define the calculation expression.
(supertag-tag-create
'(:id "task"
:name "Task"
:fields ((:name "due_date" :type :date)
(:name "completed_date" :type :date)
(:name "progress" :type :number)
;; Example: A derived number for display (not persisted)
(:name "days_left" :type :formula
:formula "10 - progress")
;; Example: Calculate completion percentage (basic arithmetic)
(:name "completion_percentage" :type :formula
:formula "(progress / 100) * 100"))))Formula fields use a single infix grammar shared by table formula fields,
formula virtual columns, and automation formula fields. Field references
are bare field names; arithmetic is + - * / with parentheses, and
/ is floating-point division (division by zero yields 0).
- Example:
"(done / total) * 100"reads thedoneandtotalfield values of the rendered node and computes a percentage. - Unknown fields resolve to their schema default (or nil/0 when unset).
- Legacy
{{key}}-placeholder prefix formulas (e.g."(- 10 {{:progress}})") are translated to the infix grammar automatically, so existing configurations keep working.
| Feature | Formula Fields | Automation Rules | Rollup |
|---|---|---|---|
| Purpose | Display calculation results in real-time in views | Modify persistent data in the underlying database based on events | Aggregate related node data, persistently store results |
| Trigger Timing | When table view is rendered | Data change events (such as property changes, tag additions/removals) | When relationships or related node properties change |
| Data Persistence | Does not store results in database | Will store results in database | Will store results in database |
| Use Cases | Lightweight, instant display calculations without changing original data | Need persistent data changes, trigger complex workflows | Cross-node data aggregation, need persistent rollup results |
In addition to responding to real-time data changes, Automation System 2.0 can also be time-driven to execute pre-scheduled tasks. This is supported by an integrated, reliable scheduler service (supertag-services-scheduler.el).
You don't need to interact with the scheduler directly. You simply define an Automation with a time rule, and the system will automatically schedule everything for you.
A scheduled task is essentially a regular Automation rule, but it must follow these conventions:
- The value of trigger (
:trigger) must be:on-schedule. - The actions in the actions (
:actions) list must be of type:call-function. Because scheduled tasks don't have a single context node, they need to call a more generic function to perform batch operations. - A
:scheduleproperty must be provided to define execution time.
The :schedule property is a property list (plist) used to precisely describe the task execution time.
| Parameter | Type | Description |
|---|---|---|
:type |
Keyword | The type of scheduled task. Currently supports :daily (daily type). |
:time |
String | The time when the task executes, in "HH:MM" format (24-hour). |
:days-of-week |
List (Optional) | A list of numbers specifying which days of the week to execute. 0 represents Sunday, 1 represents Monday, ..., 6 represents Saturday. If this parameter is omitted, the task will execute at the specified time every day. |
This is a complete, directly usable example.
Scenario: Every Friday at 8:00 PM, automatically find all incomplete high-priority tasks and add a #review tag to them for weekend review.
;; 1. Define scheduled task rule
(supertag-automation-create
'(:name "weekly-review-high-priority-tasks"
:trigger :on-schedule
:schedule (:type :daily :time "20:00" :days-of-week '(5)) ; 5 represents Friday
:actions '((:action :call-function
:params (:function #'my-app-flag-tasks-for-review)))))
;; 2. Implement the function called by the rule
(defun my-app-flag-tasks-for-review ()
"Find all incomplete high-priority tasks and add review tag."
(interactive)
(let ((tasks-to-review
(supertag-query-nodes
'(and (has-tag "task")
(not (property-equals :status "Done"))
(property-equals :priority "High")))))
(dolist (task tasks-to-review)
(let ((task-id (plist-get task :id)))
(supertag-node-add-tag task-id "review")
(message "Task %s flagged for weekly review." (plist-get task :title))))
(message "%d tasks flagged for review." (length tasks-to-review))))Important Note: To start scheduled tasks running, you must manually start the scheduler service once in your Emacs configuration file (such as init.el). This operation only needs to be performed once.
(supertag-scheduler-start)Once started, the scheduler will run continuously in the background and execute your defined scheduled tasks precisely at the times you specify.
To create your own rules, you need to understand the three core components of a rule: Triggers (WHEN), Conditions (IF), and Actions (THEN).
The trigger field defines "when" to check this rule. A precise trigger is the cornerstone of high performance.
| Trigger Type | Format | Description |
|---|---|---|
| Any Change | :on-change |
Triggered on any recognized store change event (broad; use conditions to narrow). |
| Property/Field Change | :on-property-change |
Triggered on property/field/global-field changes (broad; use property-changed / field-changed in conditions to narrow). |
| Field Change | :on-field-change |
Triggered when a global field value changes. |
| Tag Added | (:on-tag-added "tag-name") |
Triggered when a node is first tagged with the specified tag. |
| Tag Removed | (:on-tag-removed "tag-name") |
Triggered when a specified tag is removed from a node. |
| Scheduled Task | :on-schedule |
Time-based trigger, requires :schedule property and a running scheduler (see above). |
| Always / Fallback | nil / :always |
Always matches (useful for tag-only rules where the trigger is implied by the tag trigger itself). |
Notes:
:on-schedulerules are executed by the scheduler runner; they are not matched against store change events.- Unknown triggers fail closed (do not match).
The condition field defines the "preconditions" that must be met for the
rule to execute. It is the same S-expression query grammar used by query
blocks and saved queries — see doc/QUERY.md. State conditions are query
operators; event conditions are the dedicated forms below.
| Condition Type | Format | Description |
|---|---|---|
| Query grammar | (and ...) (or ...) (not ...) (tag ...) (field ...) (task ...) (priority ...) (term ...) + date operators |
Full query syntax; the rule matches the same node set a query block would |
| Property Equals (keyword) | (property-equals :prop-name "value") |
Node plist property equality (no query equivalent; keyword form only) |
| Property Changed | (property-changed :prop-name) |
This event changed the specified property |
| Property Test | (property-test :prop-name #'> 8) |
Test the property value via a function |
| Field Changed | (field-changed "field-name") |
This event changed the field's global value |
| Global Field Changed | (global-field-changed "field-id") |
This event changed that global field-id |
| Global Field Test | (global-field-test "field-id" #'pred ...) |
Test a global field value via a function |
Legacy condition forms (has-tag, has-any-tag, has-all-tags,
field-equals, global-field-equals, and property-equals with a string
key) convert deterministically to the query grammar at evaluation time and
keep working; new rules should write the query grammar directly.
In the rule plist, :condition is the IF part. It is optional, and it
must be a Lisp list form:
(supertag-automation-create
'(:name "only-when-hours-changed"
:trigger :on-property-change
:condition (and (tag "task") (property-changed :hours))
:actions '((:action :add-tag :params (:tag "touched")))))- Optional: omit it or set it to
nilto always pass (rule runs whenever:triggermatches). - Form: the query grammar plus the event-sensitive helpers above (no arbitrary
eval). - Quoting: both
:condition (and ...)and:condition '(and ...)work; the engine unwraps one leadingquote. If the whole rule is already quoted ('(...)), you typically do not need to quote:conditionagain. - Event-sensitive helpers like
property-changed/field-changedrely on the event:path(see “Event Context”), so pair them with the appropriate:trigger(e.g.:on-property-change/:on-field-change).
This guide does not define a condition-level “formula DSL”. For complex logic, compose query operators and/or move complexity into
:call-functionactions.
The actions field (note the plural) defines one or more actions that the system should execute sequentially when both trigger and conditions are satisfied. It is an action list.
Each action is a plist in the format (:action :action-type :params (...)).
Action Type (:action-type) |
:params Parameters |
Description |
|---|---|---|
:update-property |
(:property :prop-name :value new-value) |
Update or add a node property in the datastore (:properties plist). new-value is treated as a literal value. |
:update-todo-state |
(:state "new-state") |
Update the TODO state of the current node (e.g., to "DONE", "TODO"). This directly changes the headline's keyword, unlike :update-property. |
:add-tag |
(:tag "tag-name") |
Add a new tag to the current node. |
:remove-tag |
(:tag "tag-name") |
Remove a tag from the current node. |
:call-function |
(:function #'your-function :args (...)) |
Call an Emacs Lisp function you've defined yourself. This is the "ultimate weapon" for implementing complex logic. The function receives (node-id context &rest args). |
:create-node |
(:title "..." :tags '("...") ...) |
Create a completely new node. |
:update-field |
(:tag "tag-id" :field "field-name" :value v) |
Resolve the Tag schema field and update its global value for the node. |
:case |
(:on (:field "层级") :branches '((:equals "20" :actions ((:action :update-field ...))) (:default t :actions ((:action :call-function ...))))) |
Resolve a value (:on) and execute the first matching branch. Each branch can use :equals, :in, :match (regexp/function), or :test to match, and runs its own nested :actions. Provide :default t for a fallback branch. |
When a rule is executed, it receives a context plist describing the current event.
- For value changes, context commonly includes:
:path— a structured path describing what changed:old/:new— old/new values for that path
- For tag events, context commonly includes:
:tag-event— one of:added/:removed(legacy path) or:add-tag/:remove-tag(sync bridge):tag— tag name (string)
Common :path shapes used by the engine:
- Node property change:
(:nodes NODE-ID :properties :some-prop) - Global field value change:
(:field-values NODE-ID "field-id")
(property-changed ...) relies on :path being specific (e.g. (:nodes NODE-ID :properties :hours)), so the engine must preserve this precision when routing events.
Tip: A commonly useful built-in helper for :call-function is
supertag-service-org-move-node-to-file-action, which moves a node's subtree
to a target file without prompting:
(:action :call-function
:params (:function #'supertag-service-org-move-node-to-file-action
:args ("~/org/archive.org" t 1)))This example will demonstrate some features that are difficult or impossible to achieve with native Org Mode, showcasing the unique value of the new system.
(Note: This example assumes that a #task tag with status, priority, and hours fields has been pre-defined, as well as a one-to-one relationship named depends_on from task to task.)
Rule A: Automatically Set Priority Based on Estimated Hours
(supertag-automation-create
'(:name "auto-set-priority-by-effort"
:trigger :on-property-change
:condition (and
(has-tag "task")
(property-changed :hours)
(property-test :hours #'> 8))
:actions '((:action :update-property
:params (:property :priority :value "High")))))(Tip: pair :on-property-change with property-changed/property-test to keep the rule precise.)
Rule B: Automatically Unlock the Next Dependent Task When One Task is Completed
(supertag-automation-create
'(:name "unlock-next-dependent-task"
:trigger :on-property-change
:condition (and (has-tag "task")
(property-equals :status "Done"))
:actions '((:action :call-function
:params (:function #'my-app-unlock-next-task)))))
;; Implement custom function for the rule above
(defun my-app-unlock-next-task (node-id context)
"Find the next task dependent on the just-completed task and update its status from 'Waiting' to 'Todo'."
(interactive)
(let* ((completed-task-id node-id)
(dependent-tasks (supertag-relation-get-reverse-related-nodes
completed-task-id "depends_on")))
(dolist (task-info dependent-tasks)
(let ((task-id (plist-get task-info :id))
(task-data (supertag-node-get (plist-get task-info :id))))
(when (equal (plist-get task-data :status) "Waiting")
(supertag-node-update-property task-id :status "Todo")
(message "Task %s unlocked, status set to TODO." (plist-get task-data :title)))))))-
Smart Priority:
- Before Operation: Node
#task's:priority:property isLow. - Operation: Change the node's
:hours:property value from4to10. - After Operation: The node's
:priority:property automatically becomesHigh.
- Before Operation: Node
-
Dependency Unlocking:
- Before Operation: "Task A" and "Task B" are both
#task. "Task B" depends on "Task A" through thedepends_onrelationship, and "Task B"'s:status:isWaiting. - Operation: Change "Task A"'s
:status:toDone. - After Operation: "Task B"'s
:status:automatically changes fromWaitingtoTodo.
- Before Operation: "Task A" and "Task B" are both
Use the :case action when you want to map one field value to another without creating multiple rules or helper functions. The rule below watches the 层级 field on #contact nodes and adjusts 联系频率 accordingly. A default branch keeps data in a sane state if the tier is unexpected.
(supertag-automation-create
'(:name "contact-tier-frequency"
:trigger :on-field-change
;; In the global-field model, treat field-centric
;; conditions as global fields by default.
:condition '(and (has-tag "contact")
(field-changed "层级"))
:actions
'((:action :case
:params
(:on (:field "层级")
:branches
((:equals "20"
:actions ((:action :update-field
:params (:tag "contact" :field "联系频率" :value "30d"))))
(:equals "50"
:actions ((:action :update-field
:params (:tag "contact" :field "联系频率" :value "90d"))))
(:equals "150"
:actions ((:action :update-field
:params (:tag "contact" :field "联系频率" :value "180d"))))
(:default t
:actions ((:action :update-field
:params (:tag "contact" :field "联系频率" :value "90d"))))))))))Because each branch contains its own :actions list, you can chain more logic—for example, adding a notification tag, updating properties, or calling functions—once the correct branch has been selected.
The global field model is always active: fields are first-class entities and are not scoped to a single tag. The automation DSL supports rules driven by field changes instead of tags:
field-equals/field-changed– resolve the first string argument as a stable global field ID or display name.global-field-equals/global-field-changed– explicit global field variants if you want to be fully explicit.
For example, this rule fires whenever the global status field for a node becomes "done", regardless of which tags the node has:
(supertag-automation-create
'(:name "status-done-anywhere"
:trigger :on-field-change
:condition '(field-equals "status" "done")
:actions ((:action :call-function
:params (:function
(lambda (node-id _ctx &rest _)
(message "Node %s reached status=done" node-id)))))))You can still combine field-centric conditions with tag predicates when you want to narrow the scope:
:condition '(and (has-tag "task")
(field-equals "status" "doing"))Under the hood, field-equals/field-changed are indexed by the stable global field ID, so display-name changes do not move stored values or disconnect new rules from :field-values events.
This example will showcase the powerful capabilities of "Relationships" and "Rollup".
(Note: This example assumes that a #Project tag with status and total_hours fields has been pre-defined, as well as a one-to-many relationship named tasks from Project to task, configured with a sum rollup from :hours to :total_hours.)
When a subtask's status changes, we want to check if all tasks in the parent project are completed.
(supertag-automation-create
'(:name "auto-complete-project"
:trigger :on-property-change
:condition (and (has-tag "task") (property-equals :status "Done"))
:actions '((:action :call-function
:params (:function #'my-app-check-project-completion)))))
;; Implement function to check project completion status
(defun my-app-check-project-completion (task-id context)
"When a task is completed, check if all tasks in its project are completed."
(interactive)
;; 1. Find the project this task belongs to
(when-let ((projects (supertag-relation-get-related-nodes task-id "tasks" :reverse t)))
(let* ((project-id (plist-get (car projects) :id))
(all-tasks (supertag-relation-get-related-nodes project-id "tasks"))
(all-done t))
;; 2. Check if all tasks under the project are completed
(dolist (task all-tasks)
(unless (equal (plist-get (supertag-node-get (plist-get task :id)) :status) "Done")
(setq all-done nil)))
;; 3. If all tasks are completed, update project status
(when all-done
(supertag-node-update-property project-id :status "Done")
(message "All tasks in Project %s are done. Project status updated." project-id))))) -
Automatic Hours Rollup (Driven by Relationship Definition):
- Before Operation:
#Project's:total_hours:is5. It's associated with a#taskwith:hours:of5. - Operation: Associate a new
#taskwith the project and set its:hours:to3. - After Operation:
#Project's:total_hours:automatically updates to8.
- Before Operation:
-
Automatic Project Completion (Driven by This Rule):
- Before Operation:
#Projectis associated with two#tasks, one withDonestatus and another withTodostatus. - Operation: Change the
#taskwithTodostatus toDone. - After Operation:
#Project's:status:automatically becomesDone.
- Before Operation:
This is the best demonstration of the new automation engine's powerful capabilities: a single rule can execute multiple actions sequentially.
Scenario: When a task's status is set to Done, automatically record the completion date, tag it with #archived, and move it to an archive file.
(supertag-automation-create
'(:name "process-completed-task"
:trigger :on-property-change
:condition (and (has-tag "task")
(property-equals :status "Done"))
:actions
'((:action :update-property
:params (:property :completed_date :value (current-time)))
(:action :add-tag
:params (:tag "archived"))
(:action :call-function
:params (:function #'supertag-service-org-move-node-to-file-action
:args ("~/org/archive.org" t 1))))))- Before Operation: A
#tasknode with:status:ofDoing, no:completed_date:property, and no#archivedtag. - Operation: Change the node's
:status:toDone. - After Operation:
- The node gains a
:completed_date:property with the current time as its value. - The node is automatically tagged with
#archived. - The node subtree is moved into
~/org/archive.org(leaving anid:link behind at the original location).
- The node gains a
After our refactoring, the system's core philosophy has become clearer:
- Tags are Core: All data structures (fields) and behaviors (automation) are organized around tags.
- Rules are Declarative: You only need to "declare" in rules what events and targets it cares about using
:triggerand:condition, and the system will automatically apply it to the right place. - Backend is Intelligent: You don't need to worry about performance. The system automatically builds indexes for your rules, ensuring fast response times even with hundreds or thousands of rules.
- No Class Distinctions: Any tag, whether simple or complex, can have
After creating automation rules, you can use the following methods to test and debug:
;; Simulate rule triggering
(supertag-rule-execute rule node-id context)
;; Rebuild rule index
(supertag-rebuild-rule-index)
;; View current index status
(hash-table-count supertag--rule-index)The following matrix gives you small, repeatable scenarios to verify that the rule engine is wired correctly.
You can run these snippets in *scratch* after loading supertag.
(supertag-automation-create
'(:name "verify-tag-added"
:trigger (:on-tag-added "task")
:actions '((:action :add-tag :params (:tag "verified")))))
(let ((node-id (supertag-node-create :title "Verify tag-added" :tags nil)))
(supertag-ops-add-tag-to-node node-id "task" :create-if-needed t)
(message "tags=%S" (plist-get (supertag-node-get node-id) :tags)))
;; Expect: tags include "task" and "verified".(supertag-automation-create
'(:name "verify-tag-removed"
:trigger (:on-tag-removed "task")
:actions '((:action :add-tag :params (:tag "removed-task")))))
(let ((node-id (supertag-node-create :title "Verify tag-removed" :tags '("task"))))
(supertag-with-transaction
(supertag-node-remove-tag node-id "task"))
(message "tags=%S" (plist-get (supertag-node-get node-id) :tags)))
;; Expect: tags include "removed-task" and do NOT include "task".(supertag-automation-create
'(:name "verify-property-changed"
:trigger :on-property-change
:condition (property-changed :hours)
:actions '((:action :update-property :params (:property :verified_hours :value t)))))
(let ((node-id (supertag-node-create :title "Verify property-changed" :tags nil)))
(supertag-node-update-property node-id :hours 10)
(message "verified_hours=%S"
(supertag-node-get-property node-id :verified_hours)))
;; Expect: verified_hours is non-nil.(supertag-automation-create
'(:name "verify-property-test"
:trigger :on-property-change
:condition (property-test :hours #'> 8)
:actions '((:action :add-tag :params (:tag "gt8")))))
(let ((node-id (supertag-node-create :title "Verify property-test" :tags nil)))
(supertag-node-update-property node-id :hours 7)
(supertag-node-update-property node-id :hours 9)
(message "tags=%S" (plist-get (supertag-node-get node-id) :tags)))
;; Expect: tags include "gt8" (only after the 9 update).- Add Logging: Use the
messagefunction to add log output in rules - Check Trigger Conditions: Confirm that rule trigger conditions match correctly
- Validate Data Paths: Check that property names and data paths are correct
;; Add debugging information in custom functions
(defun my-debug-function (node-id context)
(message "Debug: Processing node %s with context %s" node-id context)
;; Your logic code
)
;; Check node properties
(supertag-node-get node-id)
;; Check if rule is correctly indexed
;; - property key (keyword): e.g. :hours
;; - tag name (string): e.g. "task"
(gethash :hours supertag--rule-index)
(gethash "task" supertag--rule-index)- Create Test Nodes: Create sample nodes for testing
- Trigger Events: Manually modify properties or add tags to trigger rules
- Verify Results: Check data changes after rule execution
- Debug Issues: If results don't match expectations, check triggers, conditions, and actions
;; Example: Test automatic task priority setting rule
(let ((test-node-id (supertag-node-create
:title "Test Task"
:tags '("task"))))
;; Trigger rule
(supertag-node-update-property test-node-id :hours 10)
;; Verify results
(let ((priority (supertag-node-get-property test-node-id :priority)))
(message "Priority after update: %s" priority)))Automation System 2.0 provides a series of maintenance commands to ensure healthy system operation:
;; Recalculate all rollup values
(supertag-automation-recalculate-all-rollups)
;; Synchronize all field-sync relations
(supertag-automation-sync-all-fields)
;; Clean up and rebuild index
(supertag-automation-cleanup)
(supertag-automation-init)
(supertag-rebuild-rule-index)When you need to perform the same operation on large amounts of data, you can use the following pattern:
;; Batch update priority of all incomplete tasks
(defun batch-update-task-priority ()
"Set priority of all incomplete tasks to Normal."
(interactive)
(let ((tasks (supertag-query-nodes
'(and (has-tag "task")
(not (property-equals :status "Done"))))))
(dolist (task tasks)
(let ((task-id (plist-get task :id)))
(supertag-node-update-property task-id :priority "Normal")))
(message "Updated %d tasks" (length tasks))))
;; Batch clean up archived tasks older than 90 days
(defun batch-cleanup-archived-tasks ()
"Delete archived tasks older than 90 days."
(interactive)
(let* ((cutoff-date (time-subtract (current-time) (days-to-time 90)))
(old-tasks (supertag-query-nodes
`(and (has-tag "task")
(has-tag "archived")
(property-test :completed_date
(lambda (date)
(time-less-p date ,cutoff-date)))))))
(dolist (task old-tasks)
(supertag-node-delete (plist-get task :id)))
(message "Cleaned up %d old archived tasks" (length old-tasks))));; Batch import task data from CSV
(defun import-tasks-from-csv (csv-file)
"Batch import tasks from CSV file."
(interactive "fCSV file: ")
(with-temp-buffer
(insert-file-contents csv-file)
(let ((lines (split-string (buffer-string) "\n" t)))
(dolist (line (cdr lines)) ; Skip header row
(let* ((fields (split-string line ","))
(title (nth 0 fields))
(priority (nth 1 fields))
(hours (string-to-number (nth 2 fields))))
(let ((node-id (supertag-node-create :title title :tags '("task"))))
(supertag-node-update-property node-id :priority priority)
(supertag-node-update-property node-id :hours hours)))))
(message "Tasks imported successfully")))
;; Export project summary
(defun export-project-summary (project-tag output-file)
"Export project summary report to file."
(interactive "sProject tag: \nFOutput file: ")
(let* ((projects (supertag-query-nodes `(has-tag ,project-tag)))
(report-lines '("Project Summary Report" "=========================")))
(dolist (project projects)
(let* ((project-id (plist-get project :id))
(title (plist-get project :title))
(total-hours (supertag-node-get-property project-id :total_hours))
(status (supertag-node-get-property project-id :status)))
(push (format "Project: %s | Status: %s | Hours: %s"
title status total-hours) report-lines)))
(with-temp-file output-file
(insert (string-join (reverse report-lines) "\n")))
(message "Report exported to %s" output-file)))The core performance advantage of Automation System 2.0 comes from its intelligent indexing system:
- Rule Indexing: Rules are automatically indexed based on trigger sources
- O(1) Lookup: No need to traverse all rules, achieving constant-time lookup
- Precise Matching: Supports precise matching for property changes and tag changes
- Automatic Maintenance: Indexes are automatically created and updated in the background
;; View index status
(defun supertag-check-index-status ()
"Check rule index status and statistics."
(interactive)
(let ((entry-count (hash-table-count supertag--rule-index))
(by-type (make-hash-table :test 'eq)))
(maphash (lambda (key _value)
(let* ((tkey (cond
((keywordp key) :keyword)
((stringp key) :string)
((symbolp key) :symbol)
(t :other))))
(puthash tkey (1+ (gethash tkey by-type 0)) by-type)))
supertag--rule-index)
(message "Index entries: %d" entry-count)
(maphash (lambda (type count)
(message " %s: %d" type count))
by-type)))
;; Benchmark rule lookup performance
(defun supertag-benchmark-rule-lookup (iterations)
"Benchmark rule lookup performance."
(interactive "nIterations: ")
(let ((start-time (current-time))
(test-key :status))
(dotimes (i iterations)
(gethash test-key supertag--rule-index))
(let ((elapsed (float-time (time-subtract (current-time) start-time))))
(message "Performed %d lookups in %.4f seconds (%.2f μs per lookup)"
iterations elapsed (* (/ elapsed iterations) 1000000)))));; Good practice: Use specific triggers
(supertag-automation-create
'(:name "specific-trigger"
:trigger (:on-tag-added "task") ; Specific tag trigger
:actions '((:action :update-property :params (:property :status :value "Todo")))))
;; Avoid: Overly generic triggers
(supertag-automation-create
'(:name "generic-trigger"
:trigger :on-property-change ; Generic trigger, requires additional condition filtering
:condition (has-tag "task")
:actions '((:action :update-property :params (:property :status :value "Todo")))));; Good practice: Put most likely to fail conditions first
(supertag-automation-create
'(:name "optimized-conditions"
:trigger :on-property-change
:condition (and
(property-equals :priority "High") ; Specific value match, quick failure
(has-tag "task") ; Tag check
(property-test :hours #'> 8)) ; Numeric test last
:actions '((:action :add-tag :params (:tag "urgent")))));; Dangerous: Could cause infinite loops
(supertag-automation-create
'(:name "potential-loop"
:trigger :on-property-change
:condition (property-equals :status "Done")
:actions '((:action :call-function
:params (:function #'my-app-mark-completed-and-archive))))) ; This can trigger itself if not guarded
(defun my-app-mark-completed-and-archive (node-id _context)
"Mark completion time and archive a node (example)."
(supertag-node-update-property node-id :completed_date (current-time))
(supertag-node-update-property node-id :status "Archived"))
;; Safe practice: Use different trigger conditions or avoid modifying trigger properties
(supertag-automation-create
'(:name "safe-completion"
:trigger :on-property-change
:condition (and (property-equals :status "Done")
(property-test :completed_date #'null)) ; Prevent repeated triggering
:actions '((:action :call-function
:params (:function #'my-app-mark-completed))))))
(defun my-app-mark-completed (node-id _context)
"Set :completed_date once (example)."
(supertag-node-update-property node-id :completed_date (current-time)));; Rule execution statistics
(defvar supertag--rule-stats (make-hash-table :test 'equal)
"Rule execution statistics.")
(defun supertag-track-rule-execution (rule-name execution-time)
"Record rule execution statistics."
(let ((stats (gethash rule-name supertag--rule-stats '(:count 0 :total-time 0))))
(puthash rule-name
`(:count ,(1+ (plist-get stats :count))
:total-time ,(+ (plist-get stats :total-time) execution-time))
supertag--rule-stats)))
(defun supertag-show-performance-report ()
"Display rule execution performance report."
(interactive)
(let ((report '()))
(maphash (lambda (rule-name stats)
(let* ((count (plist-get stats :count))
(total-time (plist-get stats :total-time))
(avg-time (/ total-time count)))
(push `(,rule-name ,count ,total-time ,avg-time) report)))
supertag--rule-stats)
(setq report (sort report (lambda (a b) (> (nth 2 a) (nth 2 b)))))
(with-output-to-temp-buffer "*Supertag Performance Report*"
(princ "Rule Performance Report\n")
(princ "======================\n\n")
(princ "Rule Name | Executions | Total Time | Avg Time\n")
(princ "---------|------------|------------|----------\n")
(dolist (entry report)
(princ (format "%s | %d | %.4fs | %.4fs\n"
(nth 0 entry) (nth 1 entry)
(nth 2 entry) (nth 3 entry)))))))When processing large numbers of nodes, consider the following optimization strategies:
;; Use transactions for batch operations
(defun batch-update-with-transaction (node-ids update-func)
"Batch update nodes in transaction for better performance."
(supertag-with-transaction
(dolist (node-id node-ids)
(funcall update-func node-id))))
;; Deferred calculation and caching
(defvar supertag--computation-cache (make-hash-table :test 'equal))
(defun expensive-computation-with-cache (key compute-func)
"Expensive calculation with caching."
(or (gethash key supertag--computation-cache)
(let ((result (funcall compute-func)))
(puthash key result supertag--computation-cache)
result)))Symptoms: Automation rules are defined, but don't execute when trigger conditions are met.
Possible Causes:
- Trigger doesn't match actual events
- Condition logic not satisfied
- Rules not properly indexed
Diagnostic Steps:
;; 1. Check if rule is properly indexed
(gethash "your-trigger-key" supertag--rule-index)
;; 2. Check trigger type is correct
(supertag-check-index-status)
;; 3. Test condition logic
(let ((node-id "test-node-id"))
(and (has-tag "task")
(property-equals :status "Done")))Solutions:
- Confirm trigger format is correct (e.g.,
:on-tag-addedvs(:on-tag-added "task")) - Use more specific triggers
- Check that property names and values in conditions are correct
- Rebuild rule index:
(supertag-rebuild-rule-index)
Symptoms: Slow system response, especially during data changes.
Possible Causes:
- Using overly generic triggers
- Complex condition logic
- Time-consuming operations in
:call-function - Circular dependencies causing repeated triggering
Diagnostic Steps:
;; Check performance report
(supertag-show-performance-report)
;; Benchmark rule lookup
(supertag-benchmark-rule-lookup 10000)
;; Check rule distribution
(supertag-check-index-status)Solutions:
- Use specific triggers instead of generic ones
- Optimize condition logic, putting quick-fail conditions first
- Move time-consuming operations to async tasks
- Check and eliminate circular dependencies
Symptoms: Some property values are incorrect, or rollup calculations don't match expectations.
Possible Causes:
- Race conditions from concurrent modifications
- Logic errors in rollup calculations
- Rule execution order issues
Diagnostic Steps:
;; Check node properties
(supertag-node-get node-id)
;; Manually recalculate rollup
(supertag-automation-recalculate-all-rollups)
;; Verify relationship data
(supertag-relation-get-related-nodes node-id "relation-name")Solutions:
- Use transactions to ensure atomic operations
- Avoid directly modifying storage in automation rules
- Run data consistency checks regularly
- Use synchronization mechanisms to handle concurrent access
Enable debug mode to get more detailed log information:
;; Enable verbose automation diagnostics (routine traces, SKIP/no-op notices)
(setq supertag-automation-verbose t)
;; Optional: log detailed field events in the sync bridge
(setq supertag-debug-log-field-events t)Run system health checks regularly to ensure data integrity:
(defun supertag-system-health-check ()
"Perform system health check."
(interactive)
(let ((issues '()))
;; Check rule index integrity
(unless (hash-table-p supertag--rule-index)
(push "Rule index is not properly initialized" issues))
;; Check relationship consistency
(maphash
(lambda (_ relation)
(let ((relation-name (plist-get relation :name)))
(unless (supertag-relation-validate relation-name)
(push (format "Relation %s has consistency issues" relation-name) issues))))
(supertag-store-get-collection :relations))
;; Check rollup calculations
(supertag-automation-recalculate-all-rollups)
;; Report results
(if issues
(message "Health check found %d issues: %s" (length issues) (string-join issues "; "))
(message "System health check passed successfully"))))| 1.0 API | 2.0 API | Description |
|---|---|---|
supertag-behavior-create |
supertag-automation-create |
Unified creation interface |
supertag-behavior-attach |
Automatic Indexing | No manual attachment needed |
supertag-behavior-register |
supertag-automation-create |
Modern interface |
supertag-behavior-detach |
supertag-automation-delete |
Delete rules |
(supertag-behavior-register
"task"
'(:trigger :on-property-change
:condition (property-equals :status "Done")
:action (:update-property :completed_date (current-time))))(supertag-automation-create
'(:name "complete-task-behavior"
:trigger :on-property-change
:condition (and (has-tag "task")
(property-equals :status "Done"))
:actions '((:action :update-property
:params (:property :completed_date :value (current-time))))));; Old version
(require 'supertag-ops-behavior)
(require 'supertag-automation-engine)
;; New version
(require 'supertag-automation) ; Unified module;; Migration script example
(defun migrate-behavior-to-automation ()
"Migrate behavior rules from old version to new automation system."
(interactive)
;; 1. Collect all old rules
(let ((old-behaviors (supertag-behavior-list))) ; Legacy API placeholder
(dolist (behavior old-behaviors)
(let ((tag (plist-get behavior :tag))
(trigger (plist-get behavior :trigger))
(condition (plist-get behavior :condition))
(action (plist-get behavior :action)))
;; 2. Convert to new format
(supertag-automation-create
`(:name ,(format "migrated-%s-behavior" tag)
:trigger ,trigger
:condition (and (has-tag ,tag) ,condition) ; Add tag condition
:actions (,(list :action (car action) :params (cdr action))))) ; Convert action format
;; 3. Delete old rule
(supertag-behavior-delete behavior)))
(message "Migrated %d behaviors to automation rules" (length old-behaviors))));; Verify migration results
(defun verify-migration ()
"Verify that migrated rules work correctly."
(interactive)
;; Create test node
(let ((test-node (supertag-node-create :title "Migration Test" :tags '("task"))))
;; Trigger rule
(supertag-node-update-property test-node :status "Done")
;; Check results
(let ((completed-date (supertag-node-get-property test-node :completed_date)))
(if completed-date
(message "Migration successful: completed_date = %s" completed-date)
(message "Migration failed: no completed_date set")))
;; Clean up test node
(supertag-node-delete test-node)))- Single Action vs Multiple Actions: 1.0 uses
:action, 2.0 uses:actionslist - Automatic Indexing: 2.0 no longer requires manual behavior attachment to tags
- Trigger Format: Some trigger formats have changed
- Enhanced Conditions: Need to explicitly add tag conditions
(has-tag "tag-name")
Each rule should handle only one specific scenario, avoiding multiple different logics in a single rule.
;; Good practice: Separation of concerns
(supertag-automation-create
'(:name "set-task-todo"
:trigger (:on-tag-added "task")
:actions '((:action :update-property :params (:property :status :value "Todo")))))
(supertag-automation-create
'(:name "set-task-priority"
:trigger (:on-tag-added "task")
:actions '((:action :update-property :params (:property :priority :value "Normal")))))
;; Avoid: Multiple logics in one rule
(supertag-automation-create
'(:name "setup-task-everything"
:trigger (:on-tag-added "task")
:actions '((:action :update-property :params (:property :status :value "Todo"))
(:action :update-property :params (:property :priority :value "Normal"))
(:action :call-function :params (:function #'complex-task-setup)))))Use descriptive rule names that clearly express the rule's purpose.
;; Good practice: Descriptive naming
(supertag-automation-create
'(:name "auto-archive-completed-tasks"
:trigger :on-property-change
:condition (and (has-tag "task") (property-equals :status "Done"))
:actions '((:action :add-tag :params (:tag "archived")))))
;; Avoid: Vague naming
(supertag-automation-create
'(:name "rule1"
:trigger :on-property-change
:condition (and (has-tag "task") (property-equals :status "Done"))
:actions '((:action :add-tag :params (:tag "archived")))))Use the most specific trigger types, avoiding overly generic triggers.
;; Good practice: Precise triggers
(supertag-automation-create
'(:name "handle-project-completion"
:trigger (:on-tag-added "completed")
:condition (has-tag "project")
:actions '((:action :call-function :params (:function #'celebrate-project-completion)))))
;; Avoid: Overly generic triggers
(supertag-automation-create
'(:name "handle-any-change"
:trigger :on-property-change
:condition (and (has-tag "project") (property-equals :status "completed"))
:actions '((:action :call-function :params (:function #'celebrate-project-completion)))))Avoid overly complex logical nesting, moving complex logic to custom functions.
;; Good practice: Simplified conditions
(supertag-automation-create
'(:name "flag-urgent-tasks"
:trigger :on-property-change
:condition (and (has-tag "task")
(property-equals :priority "High")
(property-test :hours #'> 8)
(not (property-equals :status "Done")))
:actions '((:action :add-tag :params (:tag "urgent")))))
;; Avoid: Complex nested conditions
(supertag-automation-create
'(:name "complex-urgent-check"
:trigger :on-property-change
:condition (and (has-tag "task")
(property-equals :priority "High")
(property-test :hours #'> 8)
(not (property-equals :status "Done")))
:actions '((:action :add-tag :params (:tag "urgent")))))Put most likely to fail conditions first to achieve quick short-circuiting.
;; Good practice: Quick-fail conditions first
(supertag-automation-create
'(:name "high-priority-task-alert"
:trigger :on-property-change
:condition (and
(property-equals :priority "Critical") ; Most selective condition
(has-tag "task") ; Tag check
(not (property-equals :status "Done")) ; Status check
(property-test :hours #'> 8)) ; Numeric test last
:actions '((:action :call-function :params (:function #'send-urgent-alert)))))For operations that need to process large amounts of data, use batch processing patterns.
;; Good practice: Batch processing
(defun batch-update-project-status ()
"Batch update project status."
(let ((projects-to-update (supertag-query-nodes
'(and (has-tag "project")
(property-equals :all-tasks-done t)))))
(supertag-with-transaction
(dolist (project projects-to-update)
(supertag-node-update-property (plist-get project :id) :status "Completed")))))
;; Avoid: Processing one by one
(supertag-automation-create
'(:name "update-each-project"
:trigger :on-property-change
:condition (and (has-tag "project") (property-equals :all-tasks-done t))
:actions '((:action :update-property :params (:property :status :value "Completed")))))Design a clear tag hierarchy structure, avoiding overly complex nesting.
;; Good practice: Clear hierarchy
;; Basic tags: task, project, note
;; Status tags: todo, doing, done, archived
;; Priority tags: low, normal, high, critical
;; Avoid: Overly complex nesting
;; task-personal-work-high-priority-urgent-due-tomorrowUse consistent field naming conventions.
;; Good practice: Standardized naming
;; Date fields: created_date, due_date, completed_date
;; Status fields: status, priority, progress
;; Relationship fields: parent_id, assigned_to, depends_on
;; Avoid: Inconsistent naming
;; create_time, dueDate, finished_at, stat, prio, progEstablish regular system health check mechanisms.
;; Daily health check
(supertag-automation-create
'(:name "daily-health-check"
:trigger :on-schedule
:schedule (:type :daily :time "02:00")
:actions '((:action :call-function :params (:function #'supertag-system-health-check)))))Track rule execution performance and frequency.
;; Performance monitoring report
(supertag-automation-create
'(:name "weekly-performance-report"
:trigger :on-schedule
:schedule (:type :daily :time "09:00" :days-of-week '(1)) ; Every Monday
:actions '((:action :call-function :params (:function #'supertag-show-performance-report)))))Regularly back up important configurations and data.
(defun backup-supertag-configuration ()
"Backup Supertag configuration."
(interactive)
(let ((backup-file (format "supertag-backup-%s.el"
(format-time-string "%Y%m%d-%H%M%S"))))
(with-temp-file backup-file
(insert ";; Supertag Configuration Backup\n")
(insert ";; Generated: " (current-time-string) "\n\n")
;; Backup tag definitions
(insert "(setq supertag-tags-backup\n")
(pp (supertag-query '(:tags)) (current-buffer))
(insert ")\n\n")
;; Backup relationship definitions
(insert "(setq supertag-relations-backup\n")
(pp (supertag-query '(:relations)) (current-buffer))
(insert ")\n\n")
;; Backup automation rules
(insert "(setq supertag-automation-rules-backup\n")
(pp (supertag-automation-list) (current-buffer))
(insert ")\n"))
(message "Configuration backed up to %s" backup-file)))