JackyJacky

Routines let your pet fetch data from APIs, parse responses, evaluate conditions, and deliver results—all defined in a simple JSON file. No compilation required.

Open Interactive Routine Builder


Quick Start

  1. Create a .json file inside the ~/.jacky/routines/ directory.
  2. Restart the pet or open Settings and click Save to trigger a config reload.
  3. Your routine will appear in the right-click context menu under 📋 Rutinas.
Tip

The system includes an example_weather.json.disabled in the default routines folder. Rename it to example_weather.json to see a working API-fetching weather routine!


Routine Types

TypescheduleHow it Runs
Manualnull or omittedTriggered by the user via: keyword match, LLM intent, or right-clicking and selecting it in the context menu.
Automatic{ "interval": <seconds> }Runs in the background on a repeating timer. Appears as read-only status in the context menu.

JSON Structure

A routine is defined by a JSON schema placed under ~/.jacky/routines/:

example_routine.json
{
  "id": "btc_check",
  "title": "Bitcoin Tracker",
  "description": "Notifies about cryptocurrency price movements",
  "enabled": true,
  "schedule": {
    "interval": 3600
  },
  "triggers": ["bitcoin", "btc"],
  "variables": {
    "threshold": "100000"
  },
  "internal_variables": {},
  "steps": [],
  "logic": [],
  "actions": {}
}

Fields Specification

  • id: String (Required). Unique identifier for the routine.
  • title: String (Required). Display name in menus and notifications.
  • description: String (Optional). Description of what the routine does.
  • enabled: Boolean (Optional, defaults to true). Set to false to disable the routine without deleting the file.
  • schedule: Object (Optional). Set to null or omit for manual routines. Set { "interval": 300 } to run every 5 minutes.
  • triggers: Array of Strings (Optional). Keywords that trigger the routine directly from user input.
  • variables: Object (Optional). Predefined variables that can be overridden dynamically by the user's natural language input.
  • internal_variables: Object (Optional). Immutable variables that cannot be overridden by the LLM.
  • steps: Array of Objects (Optional). Sequential workflow steps.
  • logic: Array of Objects (Optional). Conditional rules to select the final action.
  • actions: Object (Optional). Named final actions (e.g. text for the pet to say, notifications, or logs).

Variables & Placeholders

Variables are the glue between steps. They live in a shared context dictionary that every step can read from and write to. Use {{variable_name}} inside string attributes (URLs, headers, query params, body values, actions, etc.) to interpolate dynamic values at runtime.

Built-in Variables

These are always available in the context of any routine:

VariableValue
{{timestamp}}Current time in UTC ISO format (2024-01-15T14:30:00Z).
{{timestamp_plus_<N>m}}Now + N minutes, RFC3339 UTC (e.g. {{timestamp_plus_5m}}2024-01-15T14:35:00Z).
{{timestamp_minus_<N>m}}Now − N minutes, RFC3339 UTC (e.g. {{timestamp_minus_10m}}2024-01-15T14:20:00Z).
{{routine_id}}The routine's id
{{routine_title}}The routine's title
{{state.<key>}}Value of <key> in the routine's persistent state (returns empty string if missing).

Predefined Variables

Define default variables in the top-level "variables" object:

variables block
"variables": {
  "city": "CDMX",
  "api_key": "abc123"
}

These are loaded into context before any steps run and can be referenced as {{city}} or {{api_key}}.

Dynamic Overrides (User Input)

Every variable declared in "variables" acts as an overridable parameter. If a user triggers a routine using natural language (e.g., "tell me the weather at Paris"), the LLM automatically extracts the parameter value ("city": "Paris") and overrides the default value.

If the user does not specify a value, the default is used.

Internal & Immutable Variables

For variables that must not be exposed to the LLM or overridden by the user (like private repository endpoints, OAuth constants, or static usernames), use "internal_variables":

internal_variables block
"internal_variables": {
  "owner": "jackyclub",
  "repo": "myrepo"
}

Steps Reference

Steps execute sequentially. If a step fails, the execution halts safely.

1. HTTP Request (type: "request")

Fetches data from external API endpoints:

request step
{
  "id": "fetch_weather",
  "type": "request",
  "method": "GET",
  "url": "https://api.weatherapi.com/v1/current.json?key={{api_key}}&q={{city}}",
  "headers": {
    "Accept": "application/json"
  },
  "timeout": 10,
  "output_var": "raw_response"
}
FieldTypeDefaultDescription
idstringUnique step identifier (for logging).
typestringMust be "request".
methodstring"GET"HTTP method: GET, POST, PUT, DELETE, etc.
urlstringFull URL. Supports {{variables}}.
headersobject{}HTTP headers. Values support {{variables}}.
paramsobject{}URL query parameters. Appended to the URL.
bodyobjectnullJSON body (for POST/PUT/DELETE). Sent as Content-Type: application/json.
timeoutinteger10Request timeout in seconds.
authobjectnullOAuth 2.0 authentication block. See OAuth 2.0 section.
output_varstring""Variable name to store the raw response body text.

2. Parse (type: "parse")

Extracts values using dot-paths (JSON), tag paths (XML), or regular expressions:

parse step
{
  "id": "parse_temp",
  "type": "parse",
  "input": "{{raw_response}}",
  "parser": "json",
  "query": "current.temp_c",
  "output_var": "temp_c"
}
FieldTypeDefaultDescription
idstringUnique step identifier.
typestringMust be "parse".
inputstringThe text to parse. Usually {{some_var}} from a previous step.
parserstringParser type: "json", "xml", or "regex".
querystringParser-specific query.
output_varstring""Variable name to store the extracted value.

JSON Parser

Uses dot-path navigation. Array indices are integers (e.g. data.items.0.name).

XML Parser

Uses tag path search compatible with Python's ElementTree.find() (e.g. channel/item/title).

Regex Parser

Uses regular expression capture groups. Returns the first captured group or the full match.

Warning

Backslashes in JSON must be escaped (e.g., use \\d instead of \d).

3. Script (type: "script")

Runs a sandboxed QuickJS script to perform logic, parsing, or data transformations:

script step
{
  "id": "compute",
  "type": "script",
  "code": "const val = Number(temp_c); val * 1.8 + 32;",
  "output_var": "temp_f"
}
FieldTypeDefaultDescription
idstringUnique step identifier.
typestringMust be "script".
codestring""Inline JavaScript source. Either code or code_file is required.
code_filestring""Path to a .js file, relative to the routine .json directory.
output_varstring""Variable name to store the script's return value.

Sandbox Limits (Hardcoded)

  • Timeout: 500ms wall-clock limit.
  • Memory: 32MB heap limit.
  • Stack: 256KB stack limit.
  • No I/O: No network (fetch, XMLHttpRequest) or filesystem access.

Script Environment & Debugging

  • All context variables are loaded as JS globals.
  • Standard ES built-ins are available (Math, JSON, Date, etc.).
  • The return value of the last statement is captured.
  • console.log, console.warn, etc. are captured in the routines debug log.
Info

Scripts use quickjs to run.

4. Set State (type: "set_state")

Persists values across runs in a localized state database:

set_state step
{
  "id": "save_state",
  "type": "set_state",
  "updates": {
    "last_check": "{{timestamp}}",
    "last_temp": "{{temp_c}}"
  }
}
FieldTypeDefaultDescription
idstringUnique step identifier.
typestringMust be "set_state".
keystring""Single key to update (ignored if updates is defined).
valuestring""Value for the single key.
updatesobject{}Multi-key update map.

5. Filesystem (type: "filesystem")

Used to search or list file contents locally:

filesystem step
{
  "id": "list_files",
  "type": "filesystem",
  "query": "list_folder",
  "params": {
    "folder": "C:/Users/User/Desktop/Docs"
  },
  "output_var": "file_list"
}

Supported queries:

  • list_desktop: Lists files on the user's desktop.
  • list_folder: Lists files in a folder path defined under params.folder (supports absolute paths and variables).

6. Home Assistant Steps

Routines can interact with Home Assistant. See the Home Assistant Integration section below for details.

  • ha_resolve_entity: Resolves a natural-language query to an entity ID.
  • ha_call_service: Calls a smart-home service.

Authenticated Requests (OAuth 2.0)

Any request step can attach an auth block to automatically manage OAuth 2.0 authorization, token storage, and background refreshes.

Warning

The "connection" name specified inside the auth block (e.g., "github" or "google_calendar") must exactly match the connection name of the integration stored and configured in the application under Settings → Integrations.

oauth request
{
  "id": "fetch_profile",
  "type": "request",
  "url": "https://api.github.com/user",
  "auth": {
    "type": "oauth2",
    "connection": "github",
    "header": "Authorization",
    "scheme": "Bearer"
  },
  "output_var": "profile"
}

Connections Configuration

  1. In the app, go to Settings → Integrations.
  2. Add a connection using a preset (Google, GitHub, Microsoft, Spotify, Twitch) or Custom.
  3. Jacky launches a temporary server to securely capture the authorization token from the browser.
  4. Tokens are stored encrypted-at-rest using your OS keychain/credential manager.

Persistent State

Every routine is isolated and can maintain persistent state in <config_dir>/routine_state.json.

  • Reading: Access state values using dotted interpolation: {{state.last_temp}}.
  • Writing: Use the set_state step type to update values.

Logic

The logic block is a list of conditional rules evaluated in order. The first matching rule selects the action to run.

logic block
"logic": [
  {
    "if": { "var": "temp_c", "op": ">", "val": 35 },
    "then": "action_extreme",
    "else": null
  },
  {
    "if": {
      "and": [
        { "var": "temp_c", "op": "<", "val": 15 },
        { "var": "humidity", "op": ">", "val": 80 }
      ]
    },
    "then": "action_cold_humid",
    "else": "default"
  }
]

Operators & Nested Rules

  • Supported operators: >, <, >=, <=, ==, !=, contains.
  • Numeric comparisons automatically convert string inputs to numbers if possible.
  • Nest checks using "and" or "or" lists.
  • If "logic" is omitted or no rules match, the action named "default" is run. If there is no "default" action, no action executes.

Actions

Actions determine what the pet does with the routine's output context.

actions block
"actions": {
  "action_extreme": {
    "type": "say",
    "llm": "It is extremely hot at {{temp_c}}°C! Warn the user playfully.",
    "nollm": "🥵 Whew! It is {{temp_c}}°C outside!"
  },
  "action_log": {
    "type": "log",
    "message": "Routine ran at {{timestamp}}"
  }
}

Action Types

1. Say (type: "say")

The pet speaks.

  • llm: A prompt sent to the LLM (if enabled) to generate a natural, context-aware speech bubble.
  • nollm: Plain text used if the LLM is disabled or fails.
  • Multilingual support: Text fields can be structured as language dictionaries:
    "nollm": {
      "en": "Hello",
      "es": "Hola"
    }

2. Notification (type: "notification")

Triggers an OS system tray notification.

  • message: The notification message text.

3. Log (type: "log")

Silently writes a message into the application's routine log.

  • message: The log message text.

4. Organize (type: "organize")

Triggers a file organization confirmation flow.

  • confirm_msg: Speech bubble text the pet says while showing the files.

Triggering Manual Routines

Manual routines can be triggered in three ways:

  1. Context Menu: Right-click the pet, expand 📋 Rutinas, and click the manual routine.
  2. Keyword Triggers: If the user input matches a word in the "triggers" array, the routine runs immediately.
  3. LLM Intent: Paraphrased user commands are analyzed by the LLM to classify and map them to the correct routine.

Automatic Routines

Set a schedule interval (in seconds) to run routines automatically:

schedule
"schedule": {
  "interval": 1800
}
  • Background timers pause automatically during Gamer/Silent Mode and resume after.
  • Saving settings automatically reloads all routine configurations and restarts timers.

Home Assistant Integration

Jacky can drive a local Home Assistant (HA) instance to control lights, switches, and other entities.

Setup

  1. In the app, open Settings → Integrations → Home Assistant.
  2. Input your HA LAN URL and a Long-lived Access Token.
  3. In Settings → Permissions, ensure Control smart home (allow_smart_home) is toggled on.

Steps Reference (HA)

1. ha_resolve_entity

Resolves a natural-language query to an entity ID.

ha_resolve_entity step
{
  "id": "find_light",
  "type": "ha_resolve_entity",
  "ha_query": "{{user_query}}",
  "ha_domain_filter": "light,switch",
  "output_var": "target_entity"
}
  • Returns the resolved entity ID or an empty string if nothing matches.
  • Uses local fuzzy matching and drops back to LLM selection when local search fails.

2. ha_call_service

Calls an HA service.

ha_call_service step
{
  "id": "toggle_device",
  "type": "ha_call_service",
  "ha_entity_var": "target_entity",
  "ha_service": "toggle",
  "ha_data": {
    "transition": 2
  }
}
------
  • ha_entity_var: Variable containing the target entity ID (e.g., target_entity).
  • ha_entity_id: Literal entity ID fallback (e.g., light.living_room).
  • ha_service: Service name (e.g., turn_on, turn_off, toggle).
  • ha_domain: Service domain (inferred from entity prefix if omitted).
  • ha_data: Service data JSON payload.

Complete Example

A routine that fetches the price of Bitcoin, branches depending on whether it's above or below $100k, and reacts:

bitcoin_tracker.json
{
  "id": "btc_price",
  "title": "Bitcoin Price",
  "description": "Checks the current BTC price from CoinGecko",
  "schedule": null,
  "triggers": ["bitcoin", "btc", "crypto"],
  "enabled": true,
  "steps": [
    {
      "id": "fetch_price",
      "type": "request",
      "method": "GET",
      "url": "https://api.coingecko.com/api/v3/simple/price",
      "params": {
        "ids": "bitcoin",
        "vs_currencies": "usd"
      },
      "timeout": 10,
      "output_var": "raw_price"
    },
    {
      "id": "parse_price",
      "type": "parse",
      "input": "{{raw_price}}",
      "parser": "json",
      "query": "bitcoin.usd",
      "output_var": "btc_usd"
    }
  ],
  "logic": [
    {
      "if": { "var": "btc_usd", "op": ">", "val": 100000 },
      "then": "moon",
      "else": "normal"
    }
  ],
  "actions": {
    "moon": {
      "type": "say",
      "llm": "Bitcoin is at ${{btc_usd}} USD! It's above 100k! React excited!",
      "nollm": "🚀 BTC: ${{btc_usd}} USD — To the moon!"
    },
    "normal": {
      "type": "say",
      "llm": "Bitcoin is currently at ${{btc_usd}} USD. Make a brief comment.",
      "nollm": "₿ BTC: ${{btc_usd}} USD"
    }
  },
  "variables": {}
}

Tips & Best Practices

  • Start simple: A single request step and a "default" action are enough to get started.
  • Test APIs first: Try URLs in browser or Postman/curl before embedding them in steps.
  • Define output_var: Any step output you want to reference later must be stored in a variable.
  • Duplicate IDs: If two routine files share an id, only the first one loaded (alphabetically by filename) will be registered.