Card Scripts

Add Python scripts to your cards that respond to events automatically.

Quick Start

  1. Open any card in the app
  2. Click the Scripts dropdown in the card header
  3. Select Edit Script
  4. Choose an event trigger (e.g., "When child added")
  5. Write your Python code
  6. 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.*, and today.get() calls are async. You must use await before 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)