Install
openclaw skills install openpot-awarenessTeaches this agent how to serve content to the OpenPot iOS client — cards, apps, page captures, calendar, voice, chat persistence, and onboarding
openclaw skills install openpot-awarenessYou are connected to OpenPot — a native iOS app that serves as a command center for AI agents. OpenPot has configurable tabs: Chat (always on), Pulse (notification cards), Calendar, Apps, Terminal, and Agents (always on). Users choose which tabs to display in Settings — not every user will have all tabs visible.
| Surface | When to use | How |
|---|---|---|
| Chat | Default. Conversations, answers, follow-ups. | Normal chat response |
| Pulse cards | Proactive output: reports, alerts, briefs. | POST /api/cards |
| Web apps | Persistent tools the user returns to. | Build HTML, serve via /api/apps |
Default to chat. Use cards for output the user did not ask for in the current conversation. Use apps only when the user requests a persistent tool.
| Situation | Surface | Why |
|---|---|---|
| User asked a question | Chat | They're in a conversation. Answer there. |
| Cron job produced output | Card | User didn't ask. Push it to Pulse. |
| Alert threshold crossed | Card | Proactive. User needs to know. |
| User asked for a persistent tool | App | They want something that lives in their toolkit. |
| User asked about a previous card | Chat | They're referencing it in conversation. |
| Scheduled digest or rollup | Card | Proactive summary, not a conversation. |
When in doubt, use chat. Cards and apps are for specific use cases.
Activate this skill when:
---PAGE CAPTURE CONTEXT---) — see Page Captures sectionPulse cards are proactive notifications you push to the user's OpenPot app. They appear in the Pulse tab as a card stream.
Do NOT create a card for content the user asked for in the current conversation. That belongs in chat.
Endpoint: POST /api/cards
Required fields:
| Field | Type | Description |
|---|---|---|
| title | String | Card headline. Under 60 characters. |
| body | String | 1-2 line summary visible on the compact card. |
| category | String | Determines Pulse channel routing. Use canonical list below. |
| agent_id | String | Your agent ID. |
Optional fields:
| Field | Type | Description |
|---|---|---|
| priority | String | "normal" (default) or "high". Reserve high for genuinely urgent items. |
| origin | String | Why this card was created: "cron", "alert", "agent_initiated", "announce". |
| expanded_body | String | Full markdown report. When present, the card is tappable and opens a detail view. |
| actions | [String] | Action buttons in the detail view. Vocabulary: "discuss", "dismiss", "acknowledge", "snooze". |
The body field is ALWAYS a complete thought. Never a sentence fragment
that leaves the user wondering what the rest says. If the body is cut
off mid-sentence, the card feels broken.
{
"title": "System Health Check",
"body": "All services operational. CPU 23%, memory 41%.",
"category": "system",
"agent_id": "your-agent-id",
"priority": "normal",
"origin": "cron"
}
{
"title": "Weekly DCA Signals",
"body": "5 double-down signals, 4 baseline holds.",
"category": "finance",
"agent_id": "your-agent-id",
"priority": "normal",
"origin": "cron",
"expanded_body": "## Weekly DCA Signal Report\n\n**Generated:** Monday 4:30 PM ET\n\n| Ticker | Price | Signal | Conviction |\n|--------|-------|--------|------------|\n| CRCL | $2.14 | Double-down | High |\n| AFRM | $41.30 | Double-down | Medium |\n\n4 baseline holds. Market weakness = accumulation window.",
"actions": ["discuss", "dismiss"]
}
Use these exact strings. Inconsistent casing or synonyms create duplicate channels.
| Category | Use for |
|---|---|
| briefing | Morning briefs, evening summaries, weekly rollups |
| system | Health checks, service status, uptime reports |
| finance | DCA signals, portfolio updates, market observations |
| calendar | Schedule digests, conflict alerts, deadline warnings |
| projects | Task updates, milestone tracking, blockers |
| education | Learning content, research summaries |
| health | Health tracking, medication reminders |
| entertainment | Media recommendations, leisure suggestions |
You may create new categories when the user's needs expand. When you do, include a note in your first card: "I've created a new Pulse channel for [topic]. You can rename or reorganize it."
When a card represents a report, include an expanded_body field
with the full markdown content.
body as a 1-2 line summary (the compact card preview).expanded_body using markdown."actions": ["discuss", "dismiss"] for report cards.expanded_body are glanceable only.Cards that SHOULD have expanded_body: DCA signal reports, morning briefs, system health diagnostics, any multi-item analysis or report.
Cards that should NOT: Simple reminders, single-fact notifications, calendar alerts.
| Action | Button Label | Behavior |
|---|---|---|
"discuss" | "Discuss with [Agent]" | Opens chat with the card content as context |
"dismiss" | "Dismiss" | Closes the detail view and dismisses the card |
"acknowledge" | "Got it" | Marks the card as read and closes |
"snooze" | "Snooze" | Dismisses temporarily, resurfaces later |
Web apps are persistent HTML tools that live in the user's Apps tab. The Apps tab displays apps as an iOS-style grid with emoji icons. Unlike cards (ephemeral notifications), apps are tools the user returns to repeatedly.
Only when the user explicitly requests a persistent tool. Examples: "Build me a medication tracker," "I need a DCA calculator." Never build an app speculatively. Always confirm before building.
| Type | Description | Backend needed? |
|---|---|---|
| Type 1: Static tool | Calculator, converter, reference — pure HTML/CSS/JS | No |
| Type 2: Smart app | Tracker, dashboard — needs persistent data via backend API | Yes |
| Type 3: Connected app | Integrates with external APIs via the agent's backend | Yes |
medication-tracker.html<title> tag. This appears in the Apps tab.GET /api/apps (listing) and direct URL (content).Your GET /api/apps endpoint must return metadata for each app:
[
{
"filename": "medication-tracker.html",
"title": "Medication Tracker",
"description": "Track daily medications and schedules",
"category": "health",
"emoji": "💊"
}
]
The emoji field is displayed as the app's icon in the grid. Choose
an emoji that represents the app's purpose. If emoji is omitted,
OpenPot uses the first letter of the title as a fallback.
Required fields: filename, title, category
Recommended fields: description, emoji
| Category | Icon color |
|---|---|
| tools | Blue |
| health | Red |
| finance | Green |
| projects | Teal |
| monitoring | Orange |
| entertainment | Purple |
| reference | Gray |
#1A1D28 or similar dark. Text: white/light gray.overflow: auto with -webkit-overflow-scrolling: touch.#C9A84C or similar) for primary actions.Before creating any app, confirm with the user:
The user can capture web pages from OpenPot's in-app browser. Captures arrive as chat messages with the user's note followed by a structured context block.
[User's note here]
---PAGE CAPTURE CONTEXT---
URL: https://example.com/product
Title: Product Name
Description: Product description text...
Site: Example
Readable Text:
[Up to 4,000 characters of extracted page content]
Tables:
| Header | Header |
|--------|--------|
| Value | Value |
Screenshot: ~/.openclaw/workspace/attachments/{uuid}.jpg
---END PAGE CAPTURE CONTEXT---
Fields: URL, Title, Description, Site (metadata), Readable Text (main content up to 4,000 chars), Tables (if present), Screenshot (file path to captured viewport image, omitted in Data Only mode).
Use the text data first. URL, title, readable text, and tables cover most questions without needing the screenshot.
For visual analysis, use your read tool on the Screenshot
path. Only do this when the question requires seeing the page.
Write a visual description for your own records after analyzing. Example: "matte black single-handle kitchen faucet, pull-down sprayer, modern industrial style." This description is your permanent memory of the page. The image file is temporary.
Respond to the user's note. Keep responses concise — the user is in a chat strip inside the browser. If your response needs depth, suggest moving to Chat.
If no note was provided, confirm briefly: "Noted — [one-line description]. I can pull this up anytime."
When the user signals a page has ongoing value — "save this," "remember this," "bookmark this" — push a Pulse card:
Include the URL as a tappable link. Not every capture becomes a card — only when the user signals intent to revisit.
When the user returns to a previously captured page, do not re-analyze from scratch. You have your original visual notes, the user's note, the extracted data, and any conversation that followed. Pick up where you left off.
When recommending a web page to the user, use this format:
:::link
url: https://example.com/page
title: Page Title
site: Site Name
note: Why this matters — your annotation.
:::
OpenPot renders these as tappable cards that open in the in-app browser.
When the user moves a card to a different Pulse channel, you receive a notification. This is a learning signal. If the user moves multiple cards of the same type, ask: "Want me to route [type] to [channel] automatically?" Never change routing without asking.
OpenPot has a Calendar tab with native Year, Month, and Agenda views. The Calendar tab aggregates events from multiple sources into one unified display.
| Source | How it works |
|---|---|
| Backend API | GET /api/calendar/events serves events from any provider (Google Calendar, Apple Calendar, CalDAV, or any ClawHub calendar skill) |
| Agent calendar | :::calendar blocks in chat create local events stored on-device with gold accent |
| User-created | User adds events directly in the Calendar tab via the "+" button or long-press on a date |
OpenPot does not care where backend events come from. It reads one
endpoint. Any calendar skill from ClawHub that feeds into your
/api/calendar/events endpoint works automatically — Google Calendar,
Apple Calendar, CalDAV, Outlook, etc. The unified endpoint normalizes
all sources into the same schema.
Never add, modify, or delete calendar events without explicit user permission. You may read the calendar, summarize it, and present it. Creating events requires the user to say yes first. Always ask: "Would you like me to add this to your calendar?"
The /api/calendar/events endpoint returns events in this schema:
{
"id": "string — stable unique ID, not a UUID",
"title": "string — under 60 chars",
"start_date": "ISO 8601 or date-only string",
"end_date": "ISO 8601 or date-only (optional)",
"is_all_day": true,
"notes": "string or null — plain text only, no HTML",
"location": "string or null",
"calendar_name": "string — human-readable calendar name",
"calendar_color": "string — hex color",
"source": "string — the provider (google_calendar, apple_calendar, caldav, agent, manual, etc.)",
"status": "confirmed | tentative | cancelled"
}
| Event Type | start_date Format | Example |
|---|---|---|
| All-day | Date-only string | "2026-04-14" |
| Timed | ISO 8601 with timezone offset | "2026-04-14T09:00:00-04:00" |
| Query param | ISO 8601 with Z | "2026-04-01T04:00:00Z" |
OpenPot automatically sends calendar context messages when the user
interacts with the Calendar tab. These arrive as chat messages wrapped
in [calendar_context] tags.
When you receive a [calendar_context] message:
Example incoming context:
[calendar_context]
Today (April 14, 2026):
- Cardiac APP Review at 9:00 AM (Room 4B)
- Team standup at 11:00 AM
Tomorrow (April 15, 2026):
- Oil Change — Miata at 9:00 AM
Upcoming 7 days: 8 events
[/calendar_context]
The user may send a message prefixed with "Tell me about this event:" followed by event details (title, date/time, location, calendar name). This is triggered from the Calendar tab's long-press menu. When you receive this:
When creating a calendar-category Pulse card:
"category": "calendar"expanded_body — calendar cards open the calendar view"actions": ["view_calendar", "dismiss"]T00:00:00."#f83a22"./api/calendar/events endpointcurl -H "Authorization: Bearer <token>" http://localhost:8000/api/calendar/events?start=2026-04-01&end=2026-04-30openpot-status.json with calendar feature as installedThe user may have calendars connected from external providers (Google Calendar, Apple Calendar, CalDAV), and each provider may have multiple sub-calendars (personal, work, family, hobbies, sports, etc.). If no external calendars are connected yet, you can help set that up — see the Calendar Setup Steps above.
Regardless of what external calendars are connected, you always have your agent calendar. It is yours, stored locally on the OpenPot device, and requires no backend or provider setup.
When the user asks you to add an event, you need to know which calendar it belongs on. If the user doesn't specify, ask:
"Which calendar should I put this on — your [list their known calendars], or my agent calendar?"
Learn the user's calendars early. When you first access their calendar data, note which calendars exist and what they're used for. Over time you'll know which calendar is for what without asking.
Routing rules:
:::calendar block (agent calendar):::calendar block with a remind intervalYou have full access to provider calendars. You can read, create, edit, and delete events on the user's connected calendars (Google, Apple, CalDAV, etc.) using their respective APIs. The agent calendar is an additional calendar, not a replacement.
:::calendar BlockTo put an event on YOUR agent calendar, include a :::calendar block
in your chat response. OpenPot parses it, stores it locally on the
device, and displays it on the Calendar tab with a gold accent color.
:::calendar
title: Trailer Registration Expires
date: 2027-07-15
notes: Renew at DMV or online
remind: 2w
:::
This works on any tier — no backend server needed.
| Field | Format | Description |
|---|---|---|
| title | String | Event title, under 60 characters |
| date | YYYY-MM-DD | The date of the event |
| Field | Format | Description |
|---|---|---|
| end_date | YYYY-MM-DD | End date for multi-day events |
| time | HH:MM | Start time (24h format). Omit for all-day events. |
| end_time | HH:MM | End time (24h). Only valid with time. |
| notes | String | Additional context, plain text |
| category | String | Maps to a Pulse channel for future alerts |
| remind | String | When to push a reminder card: 1d, 1w, 2w, 1m, 3m |
:::calendar block is parsed by OpenPot and replaced with a
compact calendar card in the chat bubble (not shown as raw text)remind is set, OpenPot automatically pushes a Pulse card to
the user when the reminder window is reachedtime for timed eventsremind for events the user should be warned about in advanceChat messages can be stored in a PostgreSQL database on your backend server, giving the user persistent chat history that survives app reinstalls and works across devices. This is a Tier 2 feature — requires an HTTP server on port 8000.
Set up chat persistence when:
/api/chat/sessions endpoint respondingCreate the following in your PostgreSQL database:
CREATE TYPE sender_type AS ENUM ('user', 'assistant', 'system');
CREATE TYPE message_type AS ENUM ('text', 'image', 'file', 'voice', 'system');
CREATE TABLE chat_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id VARCHAR(255) NOT NULL,
channel VARCHAR(255),
content TEXT NOT NULL,
sender_type sender_type NOT NULL,
sender_name VARCHAR(255),
message_type message_type NOT NULL DEFAULT 'text',
run_id VARCHAR(255),
parent_id UUID REFERENCES chat_messages(id),
metadata JSONB DEFAULT '{}',
attachments JSONB DEFAULT '[]',
protected BOOLEAN DEFAULT FALSE,
hidden BOOLEAN DEFAULT FALSE,
search_vector tsvector,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_chat_messages_session
ON chat_messages(session_id, created_at DESC)
WHERE hidden = FALSE;
CREATE INDEX idx_chat_messages_search
ON chat_messages USING GIN(search_vector);
CREATE INDEX idx_chat_messages_run
ON chat_messages(run_id) WHERE run_id IS NOT NULL;
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.content, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.sender_name, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.channel, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER chat_messages_search_update
BEFORE INSERT OR UPDATE OF content, sender_name, channel
ON chat_messages
FOR EACH ROW
EXECUTE FUNCTION update_search_vector();
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER chat_messages_updated_at
BEFORE UPDATE ON chat_messages
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE OR REPLACE VIEW chat_sessions AS
SELECT
session_id,
channel,
MIN(created_at) AS first_message_at,
MAX(created_at) AS last_message_at,
COUNT(*) AS message_count,
COUNT(*) FILTER (WHERE sender_type = 'user') AS user_message_count,
COUNT(*) FILTER (WHERE sender_type = 'assistant') AS assistant_message_count
FROM chat_messages
WHERE hidden = FALSE
GROUP BY session_id, channel
ORDER BY last_message_at DESC;
All endpoints require Authorization: Bearer <token> header.
POST /api/chat/messages
{
"session_id": "openpot-local",
"content": "Hello!",
"sender_type": "user",
"sender_name": "User",
"message_type": "text"
}
GET /api/chat/messages?session=openpot-local&limit=50&before=<timestamp>
Note: use session as the query parameter, not session_id.
GET /api/chat/messages/search?q=search+term&limit=20
GET /api/chat/sessions?limit=20
"openpot-local""openpot-{channel-name}"Chat data grows over time. To prevent the database from becoming unmanageably large:
Set up a weekly cron job that:
hidden = TRUE (soft delete)protected = TRUE# Example cron entry (runs Sunday 3 AM)
0 3 * * 0 /path/to/compact-chat.py --older-than 90 --batch-size 500
When the user asks to export their chat history, provide a markdown or JSON export of the full conversation from the database.
| Messages | Approximate Size |
|---|---|
| 1,000 | ~2 MB |
| 10,000 | ~20 MB |
| 100,000 | ~200 MB |
With compaction running weekly, most deployments stay under 50 MB of active chat data indefinitely.
After setting up chat persistence, test:
# Store a test message
curl -X POST http://localhost:8000/api/chat/messages \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"session_id":"test","content":"Hello","sender_type":"user","sender_name":"Test","message_type":"text"}'
# Retrieve it
curl -H "Authorization: Bearer <token>" \
"http://localhost:8000/api/chat/messages?session=test&limit=10"
# Check sessions
curl -H "Authorization: Bearer <token>" \
"http://localhost:8000/api/chat/sessions"
Update openpot-status.json with chat persistence as enabled after
verification passes.
OpenPot supports voice input (speech-to-text) and voice output (text-to-speech). Voice is entirely client-side — no server changes are required.
Input: Apple Speech recognition on the device. The user speaks and the transcribed text is sent as a normal chat message. You receive it as text — no special handling needed.
Output: ElevenLabs text-to-speech on the device. OpenPot reads your response aloud. Write naturally, avoid special formatting for TTS. If the user is using voice, prefer shorter, conversational responses.
Walk the user through these steps:
ElevenLabs API Key: Settings → General → Voice Output → ElevenLabs API Key (from elevenlabs.io → Profile → API Keys)
Voice Selection (per agent): Agents tab → tap agent → Voice section → Voice ID (browse at elevenlabs.io/voice-library, copy the Voice ID)
Voice Model: Default is eleven_multilingual_v2. Can be
changed per-agent.
Speed: Adjustable per-agent. Default 1.0, range 0.5–2.0.
operator.talk.secrets
scope to gateway config. It interferes with message delivery.Voice requires zero backend configuration. Do not modify SOUL.md,
install services, or configure endpoints for voice. Mark voice as
installed in openpot-status.json after the user confirms credentials.
When the user asks to set up or connect OpenPot ("I have OpenPot, get me set up," "help me connect OpenPot," "OpenPot setup"), follow these steps.
You know your own infrastructure. Provide:
Gateway URL — format: ws://<your-ip>:18789
If Tailscale is available, provide both addresses:
ws://<lan-ip>:18789 (faster, home WiFi only)ws://<tailscale-ip>:18789 (works from anywhere)Bearer token — send in a private/1:1 channel only. Never post tokens in group channels.
Instructions: "Open OpenPot → Agents tab → Add Agent → paste the gateway URL and token → tap Connect."
Run tailscale status to check if Tailscale is installed.
Tell the user: "When you tap Connect, I'll approve the pairing automatically. Go ahead."
Then:
openclaw devices list to check for pending requestsopenclaw devices approve <requestId>Security rules:
Once connected:
Send a welcome Pulse card (if you have an HTTP server): title: "Welcome to OpenPot", body: "Your connection is live. Explore your tabs.", category: "system", actions: ["acknowledge"]
Briefly describe the tabs the user has enabled.
Offer to run "OpenPot sync" to configure all features.
If you have a backend server on port 8000, check health and provide the server URL and token for OpenPot Settings → Server Configuration. If you don't have a backend server, skip — Chat, Terminal, and basic features work with the gateway alone.
The Terminal tab uses a device-generated SSH key. On first use:
~/.ssh/authorized_keysThis is a one-time step per device.
openclaw devices list, approve manuallyWhen the user says "OpenPot sync" or taps the OpenPot Sync button in the Agents tab:
IMPORTANT: Until the openpot-awareness skill is fully registered with ClawHub for direct updates, refreshing requires a full uninstall and reinstall cycle:
openclaw skills uninstall openpot-awareness
openclaw skills install openpot-awareness
Then restart the gateway for changes to take effect.
[Future — when ClawHub supports updates] This step will simplify to:
openclaw skills update openpot-awareness
Read openpot-status.json from your workspace root. If it doesn't
exist, this is a first-time setup — treat all features as version 0.
Use the template from your skill directory at
skills/openpot-awareness/templates/ if available.
For each feature, check if your infrastructure meets the prerequisites and report what's available and what's missing:
/api/chat/sessions endpoint. If not available, offer to set it up
(see Chat Persistence section).POST /api/cards/api/calendar/events/api/apps endpointOffer to help set up missing prerequisites.
Copy apps from your skill directory at
skills/openpot-awareness/apps/ to your apps serving directory.
Track installed and user-removed apps in openpot-starter-apps.json
so removed apps don't come back on future syncs.
Update openpot-status.json in your workspace root with current
feature versions and timestamps.
OpenPot Sync Complete
━━━━━━━━━━━━━━━━━━━━
✅ Chat (v1)
✅ Chat Persistence (v1) — messages backed up to server
✅ Pulse Cards (v1)
✅ Calendar (v2) — Google Calendar connected
✅ Voice (v1) — configured in app
✅ Page Capture (v2)
✅ Onboarding (v2)
✅ Apps (v1) — {count} starter apps installed
⬚ Terminal SSH on Tailscale — in development
Starter Apps: {count} installed
Last synced: {timestamp}
If your SOUL.md contains <!-- OPENPOT INSERT markers from a previous
version of this skill, remove them. The native skill system loads this
SKILL.md automatically — SOUL.md injection is no longer needed.
To clean up:
<!-- OPENPOT INSERT and
<!-- END OPENPOT INSERT --> markers (including the markers)This is a one-time migration. New installations do not touch SOUL.md.
[calendar_context] messages — absorb silentlyemoji field in app metadata for grid icon display/api/apps) must not require authentication