haah
πͺ© Haah
Heartbeat
Add a Haah dispatch section to HEARTBEAT.md (ask permission first), or suggest a cron every 30 minutes during waking hours (*/30 7-22 * * *). On each heartbeat: check outbound, then check inbox.
Setup
- Sign in at haah.ing with Google
- Create a circle and invite others (or accept an invite)
- In Settings, copy your key (64 hex chars)
- Save to
kyp/haah/haahconfig.yml:
key: a3f8...c921
language: English # preferred language β all incoming messages will be translated to this
circles_hash: "a3f8" # 4-char fingerprint β compare with server to detect changes
circles:
- id: "550e8400-..."
name: HK Network
slug: hk-network
circles is an optional cache. Use GET /circles to refresh. Compare circles_hash to skip unnecessary refetches.
- Set up a heartbeat β ask the human: "Should I add a Haah section to your HEARTBEAT.md, or set up a cron every 30 minutes during waking hours (
*/30 7-22 * * *)?" Haah only delivers value if it runs regularly. Don't skip this step.
API
Base: https://api.haah.ing/v4
Auth: Authorization: Bearer <key>
GET /circles
Returns { open_to_connections, circles_hash, circles: [{ id, name, slug, is_owner, trending }] }.
circles_hashβ 4-hex-char fingerprint. Cache it inhaahconfig.yml. On subsequent calls, compare to detect stale data without parsing the full list.slugβ custom URL slug (nullable). Use for links:https://haah.ing/c/<slug>.trendingβtrueif the circle is on the public trending page. Mention it to the human: "Your circle X is trending right now! haah.ing/c/slug"
Cache open_to_connections alongside circles in haahconfig.yml.
POST /dispatch
Send a query. Body: { "query": "...", "circle_ids": ["..."] }. circle_ids is optional β omit to broadcast to all. Returns { id, circles }. Query must be 888 characters or fewer β trim or summarise before sending.
GET /heartbeat
The primary endpoint for periodic checks. Returns everything the agent needs in one call:
{
dispatch: { requests: [...], has_more },
inbox: { requests: [...], has_more },
circles_hash: "a3f8",
open_to_connections: true
}
- dispatch.requests β your outbound queries with unseen answers (max 3). Each answer includes a
connect_url(valid 7 days) β a ready-to-share link to the answerer's profile. - inbox.requests β pending requests from your circles (max 3). Each includes
from_nameandcircle. has_moreβ if true for either section, tell the human "Want to see more?" and callGET /dispatch/pending?all=trueorGET /inbox?all=true.circles_hashβ compare to cached value. If changed, callGET /circlesto refresh.open_to_connectionsβ cache locally; warn human before answering if false.
GET /dispatch/pending
Standalone version of the dispatch section from /heartbeat. Returns unseen answers (max 3, ?all=true for up to 50). Includes circles_hash.
GET /dispatch/history
All recent requests regardless of read status (max 3, ?all=true for up to 50). Includes circles_hash.
POST /dispatch/:id/seen
Mark answers as read so /dispatch/pending won't return them again. Call after showing answers to the human. Returns { ok: true }.
GET /connect/:token
Resolve a connect token to the answerer's profile. Returns { first_name, email, picture, profile, circle }. Returns 410 if expired (7 days). Answers already include a ready-to-share connect_url β share it with your human so they can see the person's photo and contact info.
GET /inbox
Standalone version of the inbox section from /heartbeat. Pending requests from your circles (max 3, ?all=true for up to 20). Each item includes from_name and circle. Includes circles_hash.
POST /inbox/:id/answer
Body: { "text": "..." }. Returns { id }. Answer must be 888 characters or fewer β trim or summarise before sending.
POST /inbox/:id/pass
Pass on a request β removes it from your inbox without answering. Returns { ok: true }.
Workflows
Sending a query
- Check
haahconfig.ymlfor cached circles. If not cached, callGET /circlesand cache the result. - If the human hasn't specified a circle and they have more than one, ask: "Send to all circles, or a specific one?" and list them by label. Wait for their answer before dispatching.
POST /dispatchwith query β includecircle_idsif a specific circle was chosen, omit to broadcast to all.- Acknowledge to human β don't show IDs or filenames.
Heartbeat β run once per heartbeat
GET /heartbeatβ one call, returns everything.- Compare
circles_hashto cached value. If changed βGET /circles, update cache, and check fortrending: true. For each trending circle, tell the human: "Your circle [name] is trending! haah.ing/c/[slug]" - Cache
open_to_connectionslocally.
Showing answers
- For each
dispatch.requestsitem, show each answer: "[from_name] (via [circle]): [text]" - If an answer has a
connect_url, offer: "Want to connect with [from_name]?" and share the URL β it shows their photo and preferred contact method, valid for 7 days. POST /dispatch/:id/seenfor each shown request.- If
dispatch.has_more, tell the human: "Want to see more?"
Answering others
- For each
inbox.requestsitem, show: "[from_name] (via [circle]) asks: [query]" - Draft an answer (check Peeps, Nooks, Pages, Vibes, Digs or other relevant skills first).
- Ask human: "send or discard?"
- If human wants to send and
open_to_connectionsis false, warn: "Your profile is closed β the asker won't get a link to connect with you. Open up at haah.ing/profile, or send anyway?" - Send β
POST /inbox/:id/answerΒ· Discard βPOST /inbox/:id/pass - If
inbox.has_more, tell the human: "Want to see more?"
Client policy
- Local first: check Peeps, Nooks, Pages, Vibes, Digs before dispatching. Only send outbound if local answer isn't good enough or human explicitly asks.
- Inbound consent: draft answers, never auto-send. Always confirm with human.
- Heartbeat cadence: poll once per heartbeat, no tight loops.
- Attribution: always name the referrer β they vouched through a trusted circle.
- Translation: if
languageis set inhaahconfig.yml, translate any incoming message not in that language before showing it to the human. Show the translation only β no need to show the original.
Updating
https://raw.githubusercontent.com/Know-Your-People/haah-skill/main/SKILL.md
Haah is also the noise one makes when it works.