Card Scripts
Add Python scripts to your cards that respond to events automatically.
Quick Start
- Open any card in the app
- Click the Scripts dropdown in the card header
- Select Edit Script
- Choose an event trigger (e.g., "When child added")
- Write your Python code
- Click Save
Events
Scripts can be triggered by six different events:
on_child_added
Triggered when a new child card is added to this card.
Extra variable: child - the newly added child card
on_tag_added
Triggered when a tag is added to a card. Can optionally filter by specific tag name.
Extra variable: tag_name - the tag that was added
on_tag_removed
Triggered when a tag is removed from a card. Can optionally filter by specific tag name.
Extra variable: tag_name - the tag that was removed
on_data_changed
Triggered when a card's custom field values change (database cards).
Extra variables: old_data, new_data - dictionaries with field values
on_manual_trigger
Triggered when you click the "Run" button in the script editor.
No extra variables - useful for testing scripts
on_button_click
Triggered when a custom button on the card is clicked.
Extra variable: button_id - the ID of the clicked button
Variables
Every script has access to the card variable representing the card the script is attached to:
| Property | Type | Description |
|---|---|---|
card.id | string | Unique identifier of the card |
card.name | string | null | Card title/name |
card.content | string | Card content (text/markdown) |
card.data | dict | Custom field values (for database cards) |
card.parentId | string | null | ID of the parent card |
card.tags | list[string] | List of tag names on this card |
card.children | list[card] | List of child cards |
API Reference
cards
Functions for working with cards. All functions are async - use await.
| Function | Description |
|---|---|
await cards.get(id) | Get a card by its ID |
await cards.get_by_name(name, parent_id?) | Find a card by name (optionally within a parent) |
await cards.find(query) | Search for cards matching a query |
await cards.children(card_id) | Get all children of a card |
await cards.create(name, parent_id?, data?) | Create a new card |
await cards.update(card_id, data) | Update a card's custom field values |
await cards.set_title(card_id, name) | Update a card's title/name |
await cards.set_content(card_id, content) | Replace a card's content |
await cards.append_content(card_id, text) | Append text to a card's content |
await cards.delete(card_id) | Delete a card |
tags
Functions for working with tags. All functions are async - use await.
| Function | Description |
|---|---|
await tags.add(card_id, tag_name) | Add a tag to a card |
await tags.remove(card_id, tag_name) | Remove a tag from a card |
await tags.list(card_id) | Get all tags on a card |
await tags.has(card_id, tag_name) | Check if a card has a specific tag |
today
Functions for working with daily notes:
| Function | Description |
|---|---|
await today.get() | Get today's daily note card (creates if needed) - async |
today.get_date_string() | Get today's date as a string (e.g., "29-Jan-2026") - sync |
log
Functions for logging (viewable in Tools > Script Logs). These are synchronous - no await needed.
| Function | Description |
|---|---|
log.debug(message, data?) | Log a debug message (with optional data) |
log.info(message) | Log an info message |
log.warn(message) | Log a warning message |
log.error(message) | Log an error message |
popup
Show popup notifications to the user. Synchronous - no await needed.
| Function | Description |
|---|---|
popup.show(message) | Show an info popup (blue, 5 seconds) |
popup.show(message, "success") | Show a success popup (green) |
popup.show(message, "warning") | Show a warning popup (amber) |
popup.show(message, "error") | Show an error popup (red) |
popup.show(message, level, duration) | Custom duration in ms (0 = persistent until dismissed) |
Example:
popup.show("Task completed!", "success")
popup.show("Warning: Low inventory", "warning", 10000) # 10 seconds
popup.show("Error occurred", "error", 0) # Stays until dismissed Examples
Auto-tag new children
When a child card is added, automatically tag it as "inbox":
# Event: on_child_added
await tags.add(child.id, "inbox")
log.info(f"Tagged new card '{child.name}' as inbox") Add to today's daily note
When triggered, append a timestamped entry to today's note:
# Event: on_button_click
from datetime import datetime
today_card = await today.get()
timestamp = datetime.now().strftime("%H:%M")
entry = f"\n- [{timestamp}] {card.name}"
await cards.append_content(today_card.id, entry)
log.info("Added entry to daily note") Compare inventory and create grocery list
Compare current inventory with desired state and create a shopping list:
# Event: on_button_click
# Find inventory cards by name
desired = await cards.get_by_name("PERFECT INVENTORY")
current = await cards.get_by_name("CURRENT INVENTORY")
if not desired or not current:
log.error("Inventory cards not found")
else:
# Get items from each inventory
desired_items = {c.name: c.data.get("quantity", 1) for c in desired.children}
current_items = {c.name: c.data.get("quantity", 0) for c in current.children}
# Calculate what's missing
missing = []
for item, needed in desired_items.items():
have = current_items.get(item, 0)
if have < needed:
missing.append(f"- {item}: need {needed - have} more")
# Add to today's note
if missing:
today_card = await today.get()
grocery_list = "\n\n## Grocery List\n" + "\n".join(missing)
await cards.append_content(today_card.id, grocery_list)
log.info(f"Added {len(missing)} items to grocery list")
else:
log.info("Inventory is complete!") React to data changes
When a card's status field changes to "done", add a completed tag:
# Event: on_data_changed
old_status = old_data.get("status", "")
new_status = new_data.get("status", "")
if old_status != "done" and new_status == "done":
await tags.add(card.id, "completed")
log.info(f"Marked '{card.name}' as completed") Archive card on button click
Create a custom "Archive" button that moves the card:
# Event: on_button_click
if button_id == "archive":
# Find or create archive folder
archive = await cards.get_by_name("Archive")
if not archive:
archive = await cards.create("Archive")
# Move card to archive (update parent)
await cards.update(card.id, {"parentId": archive.id})
await tags.add(card.id, "archived")
log.info(f"Archived '{card.name}'") Custom Buttons
You can add up to 5 custom buttons to a card. Primary buttons (up to 2) appear in the card header; others appear in a dropdown menu.
Button Properties
- Label: The button text (keep it short)
- Icon: An emoji to display (optional)
- Primary: Show in header (true) or dropdown (false)
- Confirm: Optional confirmation message before running
- Script: Each button can have its own script, or use the card's main on_button_click script
Technical Notes
- Runtime: Scripts run in Pyodide (Python in WebAssembly) directly in your browser
- Execution: Scripts run one at a time (single-threaded). Long-running scripts may block other scripts
- Rate limiting: A card+event combination can run at most 5 times per second (200ms minimum between runs)
- Async (IMPORTANT): All
cards.*,tags.*, andtoday.get()calls are async. You must useawaitbefore them, e.g.,card = await cards.get_by_name("My Card") - Logging: All script output is saved for 30 days. View it in Tools > Script Logs
- First run: The first script execution may be slow as Pyodide loads (~3-5 seconds)