# Text Expander Keyboard > Text Expander Keyboard is an iOS app + custom keyboard for storing reusable text snippets ("replies") and inserting them anywhere you type. Replies support dynamic "magic variables" (date, time, clipboard, random choice, cursor position) and can be organized with color-coded tags. The app can import and export a reply collection as JSON, which makes it possible for an AI agent to generate a ready-to-import library of replies for a user. This file describes the app's features and the exact JSON import/export format so an AI agent can generate a `.json` file a user can import directly in the app (Settings → Import Collection). ## What the app does - **Replies**: short or long pieces of reusable text (greetings, support answers, email templates, signatures, addresses, etc.). Inserted via a custom iOS keyboard in any app. - **Magic variables**: placeholders inside a reply's text that are replaced with live values when the reply is inserted. - **Tags**: color-coded groups used to organize and filter replies (e.g. "Customer Support", "Sales"). - **Import/Export**: the entire collection can be exported to JSON and imported from JSON or CSV. This is the integration point for AI agents. ## Generating replies for a user (the useful part for AI agents) When a user asks an AI agent to "create text expander replies for X", the agent should produce a single JSON document in the import format below. The user saves it as a `.json` file and imports it via **Settings → Import Collection** in the app. ### JSON import format Two shapes are accepted: 1. **Wrapper format (recommended — supports tags):** ```json { "version": "1.2", "tags": [ { "id": 1, "title": "Customer Support", "color_index": 5 }, { "id": 2, "title": "Sales", "color_index": 2 } ], "replies": [ { "title": "Greeting", "text": "Hi there! Thanks for reaching out. How can I help you today?", "type": "text", "group": 1, "sorting_key": 0 }, { "title": "Follow-up", "text": "Just following up on our chat. Can you talk on %%DATE +2d%%?", "type": "text", "group": 2, "sorting_key": 1 } ] } ``` 2. **Flat array format (replies only, no tags):** ```json [ { "title": "Thank you", "text": "Thanks so much — really appreciate it!", "type": "text" }, { "title": "Let me check", "text": "Let me look into that and get back to you shortly." } ] ``` ### Field reference **Reply object:** | Field | Type | Required | Notes | |-------|------|----------|-------| | `title` | string | yes | Short label shown on the keyboard button. | | `text` | string | yes | The content inserted. May contain magic variables and `\n` newlines. | | `type` | `"text"` \| `"image"` | no | Defaults to `"text"`. Only generate `"text"` — image replies cannot be created from JSON. | | `group` | integer | no | A single tag's `id`. Links the reply to one tag. | | `tag_ids` | integer[] | no | Multiple tag ids. Takes precedence over `group` when present. | | `sorting_key` | integer | no | Display order (ascending). Defaults to 0; give each reply an incrementing value to control order. | `id`, `created_at`, `updated_at` may be present in exports but are optional on import and can be omitted. **Tag object:** | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | integer | yes | Referenced by a reply's `group` / `tag_ids`. Unique within the file. | | `title` | string | yes | Tag name. On import, tags are matched to existing ones by title (case-insensitive); a new tag is created if no match exists. | | `color_index` | integer | yes | Color of the tag. See palette below (0–19). | **Tag `color_index` palette:** `0` maroon, `1` yellow, `2` purple, `3` orange, `4` red, `5` blue, `6` green, `7` teal, `8` pink, `9` indigo, `10` mint, `11` brown, `12` cyan, `13` lime, `14` coral, `15` lavender, `16` gold, `17` navy, `18` rose, `19` gray. ## Magic variables Magic variables are placeholders written as `%%NAME%%` inside a reply's `text`. They are replaced with live values at the moment the reply is inserted. They are **case-insensitive**. | Variable | Result | |----------|--------| | `%%DATE%%` | Current date in the device's locale (medium style). | | `%%TIME%%` | Current time as `HH:mm`. | | `%%DATETIME%%` | Current date + time (locale medium date, short time). | | `%%CLIPBOARD%%` | Current clipboard text (removed if clipboard is empty). | | `%%CURSOR%%` | Marks where the cursor lands after insertion (inserts nothing). | | `%%RANDOM:a\|b\|c%%` | One option chosen at random from the `\|`-separated list. | ### Date/time formatting and offsets - **Custom date format:** `%%DATE:format%%` where `format` is a Unicode/`DateFormatter` pattern. - `%%DATE:yyyy-MM-dd%%` → `2026-06-01` - `%%DATE:EEEE%%` → `Monday` - `%%DATE:MMM d%%` → `Jun 1` - (`%%TIME%%` and `%%DATETIME%%` do **not** accept a custom format — only offsets.) - **Offsets:** add a **space**, then a sign and one or more unit chunks. Units: `W` weeks, `D` days, `H` hours, `M` minutes, `S` seconds (case-insensitive). Combine freely. - `%%DATE +1d%%` → tomorrow's date - `%%DATE +1w%%` → one week from now - `%%TIME +2h30m%%` → time 2h30m from now - `%%DATETIME -1d%%` → yesterday, date + time - `%%DATE:yyyy-MM-dd +10d%%` → ten days from now in ISO format **Important syntax rules (generate exactly this way):** - The offset must be separated from the variable by a single space and begin with `+` or `-` immediately followed by a digit: `%%DATE +1d%%` is valid; `%%DATE:+1d%%` is **not** (a colon introduces a *format*, not an offset). - Do not write natural-language dates like `%%DATE:next Monday%%` — they are not parsed. ## Example: a small ready-to-import collection ```json { "version": "1.2", "tags": [ { "id": 1, "title": "Customer Support", "color_index": 5 }, { "id": 2, "title": "Scheduling", "color_index": 6 } ], "replies": [ { "title": "Ticket received", "text": "Thanks for contacting us. Your request was received and assigned ticket #%%RANDOM:4820|5913|6047%%. We'll reply within 24 hours.", "type": "text", "group": 1, "sorting_key": 0 }, { "title": "Escalation", "text": "I've escalated your case to our senior team. Expect an update by %%DATE +1d%%. Thanks for your patience.", "type": "text", "group": 1, "sorting_key": 1 }, { "title": "Propose meeting", "text": "Would %%DATE:EEEE, MMM d +2d%% at 2:00 PM work for a quick call? %%CURSOR%%", "type": "text", "group": 2, "sorting_key": 2 } ] } ``` ## Guidance for AI agents - Output a **single valid JSON document** in the wrapper format. Do not wrap it in extra prose inside the file. - Use `type: "text"` for everything; image replies are not importable from JSON. - Keep `title` short (a few words) — it is the keyboard button label. Put the full content in `text`. - Use `\n` for line breaks inside `text`. - Group related replies under tags and reference them via `group` (single tag) or `tag_ids` (multiple tags). - Use incrementing `sorting_key` values to control display order. - Only use magic variables when dynamic content genuinely helps; prefer the exact syntax documented above. ## Links - Full reference (expanded): https://expander.rdnv.io/llms-full.txt - Guide: https://expander.rdnv.io/guide/ - Privacy Policy: https://expander.rdnv.io/privacy/ - Terms & Conditions: https://expander.rdnv.io/terms/