---
name: oetzi
description: Work an Oetzi workspace as an outbound agent. Pull intent-scored conversations from social listening, the people who wrote and engaged with them, share-of-model gaps, and the post queue, then act (reply, draft, publish, record outreach) through one gated API. Use when the user mentions Oetzi, a workspace API key (oetzi_...), conversations to reply to, people to reach, or posts to draft.
---

# Oetzi

Oetzi finds the conversations that matter to a product across X, Reddit,
LinkedIn, Threads, Bluesky, Hacker News, and Substack Notes, scores each for
buying intent, builds a graph of the people around them, tracks which AI
engines recommend the product, and keeps a queue of posts and replies.
You are the outbound agent: Oetzi supplies signal and people, you act.

## Connect

One workspace API key (`oetzi_...`, minted by a human under Settings) with
scopes: read, write, spend (may use credits), post (may publish).

- MCP (preferred): `claude mcp add --transport http oetzi https://www.oetzi.ai/api/mcp --header "Authorization: Bearer $OETZI_API_KEY"`
- REST: `POST https://www.oetzi.ai/api/v1/capabilities/{name}` with a JSON body, same names as the MCP tools. Spec: https://www.oetzi.ai/api/v1/openapi.json
- CLI: `npx @oetzi/cli login` then `oetzi run {name} --key=value`

## Rules

1. Start every session with `get_playbook`, then `list_signals`. Never rely on a remembered signal list.
2. Poll `get_leads status=new band=hot` at most every 15 minutes. Read `get_lead` before acting.
3. Before contacting anyone, `get_person` and check `outreach`. Never double-contact.
4. After acting, `mark_lead status=contacted` and `record_outreach` (channel, message, outcome, your name). This is mandatory.
5. Capabilities that spend credits return a dry-run estimate until called with `approved: true` and `max_credits`. Ask your operator before approving anything above 2 credits, and stop on `budget_exceeded` or `insufficient_credits`.
6. Publishing (`publish_post`, `post_reply`) needs a post-scoped key and `approved: true`. Only publish text the operator approved or a standing instruction covers.
7. Long work returns `job_id`. Call `wait_job` until status is done or error.
8. Push: `wait_events` after a cursor from `list_events` for `lead.hot`, `person.discovered`, `post.published`, `reply.received`, `outreach.recorded`.
9. Rate matches honestly with `rate_lead`. Labels tune the judge overnight.

## The loop

```
get_playbook → list_signals → get_leads(status=new, band=hot)
  → get_lead → draft_reply → (post_reply | reply yourself) → record_outreach → mark_lead
  → pull_engagers on the strong ones → list_people(lead_id) → resolve_person for LinkedIn people worth email
```

## Errors

approval_required, budget_exceeded, insufficient_credits, scope_denied,
not_found, invalid_input, rate_limited, conflict, unsupported, internal.
scope_denied means ask the operator for a wider key, never retry.

## Capabilities (124)

## Discovery

### capabilities_search

Find capabilities by what you want to do (for example "people who liked a post" or "pause a signal"). Returns names, scopes, and whether each spends credits. Follow with capabilities_schema for exact inputs.

_scope: read_

Parameters:
  - query: string · Free text. Omit to list everything.
  - limit: integer · 1..50
Example input: `{"query":"run a sweep now"}`

### capabilities_schema

Exact input schema (JSON Schema) for one capability, plus its scope, whether it spends credits, and whether it returns a job_id.

_scope: read_

Parameters:
  - name (required): string
Example input: `{"name":"run_signal_now"}`

## Workspace

### get_workspace

This workspace's name, plan, credit balance (credits are the meter for sweeps, checks, renders, and pulls), and the scopes of the key you are using. Check the balance before spending.

_scope: read_

No parameters.

### get_playbook

The out-of-the-box playbook for this workspace: the current signal list, the recommended loop (refresh, poll, act, mark), and the spending rules. Call this first in a new session, and re-call it to refresh your view of what the workspace is listening for.

_scope: read_

No parameters.

### get_trust

What Oetzi is allowed to do on its own in this workspace, per action kind (post:<platform>, reply, video, topics): rung 0 asks first, rung 1 does it after a window unless told no, rung 2 does it and reports. Also the streak of clean approvals toward the next proposal and the last change with its reason. Free.

_scope: read_

No parameters.

### set_trust

Move one action kind to a rung, or reset every kind to 0 ("ask me again"). Only the workspace owner does this, and Oetzi never moves a rung up on its own: it proposes after a streak and the person says yes. kind is post:<platform>, reply, video or topics. window_hours is how long a rung-1 notice gives for a no (default 4). Free.

_scope: write_

Parameters:
  - kind: string
  - rung: integer · 0..2
  - window_hours: integer · 1..48
  - reset: boolean · true = every kind back to rung 0
  - reason: string
Example input: `{"kind":"post:linkedin","rung":2}`

## Publishing

### hold_post

Hold a post Oetzi scheduled on its own (rung 1, "unless you say no"): it goes back to a draft and waits for a tap. Pass post_id, or platform to hold the next one there, or nothing to hold the soonest. Free.

_scope: write_

Parameters:
  - post_id: string · uuid
  - platform: string

### list_social_accounts

Accounts this workspace can publish through: natively connected X, LinkedIn, Bluesky, and a Postiz account with its channels (TikTok, Instagram, YouTube, Threads, and more). Replies need a native account; Postiz publishes originals and video only.

_scope: read_

No parameters.

### list_posts

The post queue: drafts waiting for review, approved and scheduled posts, what was published (with URLs), failures with their error. Filter by status, platform, kind (original | reply), or a GEO plan item key.

_scope: read_

Parameters:
  - status: string · one of: draft | approved | scheduled | posted | failed | rejected
  - platform: string
  - kind: string · one of: original | reply
  - plan_item_key: string
  - limit: integer · 1..200
Example input: `{"status":"draft","limit":20}`

### get_post

One queued or published post in full, with its topic, rationale, reply target, media, and publish result.

_scope: read_

Parameters:
  - post_id (required): string · uuid · Post id from list_posts

### create_post

Put a post you wrote into the queue as a draft (or already approved) for a platform: x, linkedin, bluesky, threads, or any Postiz channel provider (tiktok, instagram, youtube). Publish it with publish_post. Character limits are enforced per platform.

_scope: write_

Parameters:
  - platform (required): string
  - body (required): string
  - approve: boolean · true = lands approved instead of draft (still needs publish_post to go out)
  - media_from_content_id: string · uuid · Attach a rendered video or carousel from list_content
  - visual: object · Draw a proof card and attach it (a few seconds, no extra credit)
  - rationale: string
Example input: `{"platform":"linkedin","body":"We crossed 1,000 conversations found this quarter. Here is what changed.","approve":false,"visual":{"kind":"stat","label":"Conversations found","value":"1,284","series":[120,180,260,410,640,1284],"from":"Apr","to":"Sep"}}`

### make_card

Draw or redraw the graphic on a queued post: a stat card (a real number, optionally a series as a line), a quoted line with its platform, one sharp sentence, or a workspace screenshot, on a wallpaper. Pass a visual to draw a new one, a wallpaper alone to redraw the current one, or remove: true to take it off. Free: a still in our renderer, no vendor.

_scope: write_

Parameters:
  - post_id (required): string · uuid · Post id from list_posts
  - visual: object · A graphic drawn for the post: stat needs label + value, quote needs text, text needs headline, screenshot needs key. Numbers must be real.
  - wallpaper: string · one of: aurora | dusk | mint | graphite | paper | ember | sunrise | ocean
  - remove: boolean
Example input: `{"post_id":"00000000-0000-0000-0000-000000000000","wallpaper":"ember"}`

### draft_posts

Draft original posts in the workspace's voice for one platform, from a topic: a row on the Topics list (topic_id, see list_topics), something trending today bridged to the buyers (trending: true, see list_trends), a share-of-model plan item (plan_item_key from get_share_of_model), a brief, verbatim hot-match phrasings (lead_ids), or a seed post to riff on. Drafts land in the queue for review. Pass `voice` to write in one of the workspace's voices (list_voices), otherwise the default writes. 1 credit per run (one platform, up to 3 variants). Running the same topic on the same platform again is free, and seed_post_id (more like this) is always free. A run that produces nothing refunds itself. Returns a job_id.

_scope: spend · spends credits (draft) · async, returns job_id_

Parameters:
  - platform (required): string
  - count: integer · 1..3
  - brief: string
  - plan_item_key: string
  - lead_ids: array
  - seed_post_id: string · uuid
  - trending: boolean · true = the best-fitting trend from today's scan, bridged to the buyers
  - topic_id: string · uuid · A row on the Topics list (list_topics): your own news or a promoted trend
  - voice: string · Which voice writes it: a slug or id from list_voices. Omit for the workspace default. A company page wants the brand voice, a personal feed wants that person's.
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"platform":"linkedin","plan_item_key":"post:...","count":2}`

### run_newsroom

Draft today's story for every page now instead of waiting for the morning: on each platform with a plan on, every connected page (a presence's channel, or the account) gets its best article published today (the workspace's newsroom rules decide the window) from its own site feeds, as a post with the article's own picture (never a map), its first two paragraphs verbatim and the link as the first comment. The page's own recent posts are read first, so an article already shared by hand is skipped. One per page per day, never a second. Free, no model call. The drafts wait on Posts and in the morning standup ("yes N" over text posts one); under trust rung 1 or 2 they schedule themselves. Also expires yesterday's unposted stories. The result names the rules in force; change them with set_newsroom_rules.

_scope: write_

No parameters.
Example input: `{}`

### set_newsroom_rules

The newsroom's standing rules for this workspace, read and set. window: today (default, the owner's calendar day: a page whose edition published nothing today gets no story) or day_and_a_half. images: article (the article's own picture, the first that is not a map and not on the never list) or none. never_images: kinds of picture never used (map, stock, logo, icon, avatar, screenshot, chart, or a word that must not appear in the image URL); new words join the list, reset clears it first. check_page: read the page's own posts of the last 14 days (Postiz, and LinkedIn through Alexandria when a free tool lists them) and skip an article it already shared, even by hand. Pass nothing to read. Over text the same rules are sentences: "today's articles only", "never a map as the picture", "check what the page already posted". Free.

_scope: write_

Parameters:
  - window: string · one of: today | day_and_a_half
  - images: string · one of: article | none
  - never_images: array
  - check_page: boolean
  - reset: boolean · Start from the defaults before applying the rest
Example input: `{"never_images":["map"],"window":"today"}`

### review_post

Approve, reject, or edit a queued post. Rejections and edits can carry feedback, which becomes a standing rule for future drafts. Approving does not publish, call publish_post for that.

_scope: write_

Parameters:
  - post_id (required): string · uuid · Post id from list_posts
  - action (required): string · one of: approve | reject | edit
  - body: string · New text, for action=edit
  - feedback: string · Why, kept as a standing rule for the drafter
Example input: `{"post_id":"<post id>","action":"reject","feedback":"Too salesy, never open with the product name"}`

### cross_post

Take one queued or published post and draft it for other platforms: the same claims, numbers, and links reshaped to each platform's length and norms, with the original's graphic or video carried over. Targets: linkedin, x, bluesky, threads, substack (Substack Notes, copy-only, no API), mastodon, facebook, instagram, and tiktok or youtube when the source has a video. Each adaptation lands in the queue as a draft (approve: true lands them approved), publish each with publish_post. Free. Returns a job_id.

_scope: write · async, returns job_id_

Parameters:
  - post_id (required): string · uuid · The post to reshape, from list_posts
  - platforms (required): array · Where it should also go, its own platform is skipped
  - approve: boolean · true = the adaptations land approved instead of draft (still need publish_post)
Example input: `{"post_id":"<post id>","platforms":["linkedin","substack","bluesky"]}`

### publish_post

Publish a queued post under the workspace's name through the connected account for its platform (Postiz for its channels). Marks it approved by you. Optional schedule_for (ISO) queues it for later. Free, but requires approved: true and a post-scoped key. Returns a job_id unless scheduled.

_scope: post · async, returns job_id_

Parameters:
  - post_id (required): string · uuid · Post id from list_posts
  - schedule_for: string · date-time
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"post_id":"<post id>","approved":true,"max_credits":0}`

### post_reply

Reply to a matched conversation on its platform, under the workspace's connected account (X, LinkedIn comment, Bluesky). Pass the text, or omit it to send the first draft_reply variant. Marks the conversation contacted and records the outreach. Free, requires approved: true and a post-scoped key.

_scope: post · async, returns job_id_

Parameters:
  - lead_id (required): string · uuid
  - text: string
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"lead_id":"<lead id>","text":"Same here, we moved off X last year because...","approved":true,"max_credits":0}`

### get_posting_plan

The posting plan per platform: how many original posts a week Oetzi drafts (0 = off, capped per platform), whether each draft waits for approval or publishes itself (publish_mode approve | auto), the credits a month that implies, and when it last ran. One in three posts is topical (today's trends bridged to the buyers), the other two deliver knowledge.

_scope: read_

No parameters.

### set_posting_plan

Set the posting plan for one platform: posts_per_week (0 = off, clamped to the platform's daily ceiling times seven) and publish_mode. approve (default) leaves every draft in the queue for a tap. auto publishes each draft under the workspace's name about half an hour after it is written, only when an account can publish there. 1 credit per drafted post. A raise saves as asked and comes back with advice (plan.advice) when the numbers cannot back it yet: raise a fifth at a time, wait for a week-old read, fix the page clicks land on before more volume.

_scope: write_

Parameters:
  - platform (required): string
  - posts_per_week (required): integer · 0..70
  - publish_mode: string · one of: approve | auto
Example input: `{"platform":"linkedin","posts_per_week":7,"publish_mode":"approve"}`

### get_post_performance

The scoreboard: what each published post did (impressions, likes, comments, shares, clicks, from Postiz analytics, the platform, or the workspace's own engager pulls at 24h, 72h, and 7 days), ranked by engagement score, with averages by topic kind, by content angle, and by platform, plus the latest 7-day snapshot per channel. scorecard holds the verdict per platform: each angle and topic kind is untested until 3 posts have a settled week-old read the platform actually showed (under 1,000 impressions is starved, not a loss), then winner (twice the workspace average), middle, loser, or retired (resting from the rotation for a season). Verdicts rest on recorded outcomes (replies, signups, customers) once the workspace has a few, on engagement until then. Free.

_scope: read_

Parameters:
  - days: integer · 1..90 · Window, default 30
  - platform: string
  - limit: integer · 1..200
Example input: `{"days":30}`

### refresh_post_metrics

Read a published post's numbers right now (Postiz analytics for Postiz posts, the latest engager pull otherwise) and store them as the post's manual snapshot. Free.

_scope: write_

Parameters:
  - post_id (required): string · uuid · Post id from list_posts

### set_angle_state

Rest a content angle from the posting rotation on one platform for a season (about 90 days), or put it back early. The nightly review rests a loser by itself once it has a fair sample; this is the person's override, and the review never overturns it. Free.

_scope: write_

Parameters:
  - platform (required): string
  - angle (required): string · The angle text exactly as the brain lists it
  - state (required): string · one of: retired | active
  - reason: string
Example input: `{"platform":"linkedin","angle":"point-tool fatigue: paying five subscriptions to do one job","state":"retired","reason":"we stopped selling on price"}`

### list_trends

What is being talked about right now that fits this workspace's buyers: today's trend scan (corpus velocity leaders plus a live news search, scored by the judge for ICP fit, each with the bridge line from headline to buyer problem). Scanned once a day per workspace, free. Pass refresh: true to scan again today. Draft on one with draft_posts trending: true.

_scope: read_

Parameters:
  - refresh: boolean

### enable_channel

Turn one Postiz channel on or off for this workspace, by channel id from list_social_accounts. On means the cadence can post to it and its numbers come back. Turning one on here releases it from any sibling workspace that had it. Free.

_scope: write_

Parameters:
  - channel_id (required): string
  - enabled (required): boolean
Example input: `{"channel_id":"cmu27bcgi00fzoi0ypq2g0rr5","enabled":true}`

### audit_formats

Read the workspace's last 90 days of posts on every platform it can publish to, including posts published outside Oetzi through Postiz, tag each with the shape it takes (article lead + link, one number, a take, how to, a story, a question, a list, team news, someone's words), and judge the shapes with fair-test rules. Writes the playbook (get_formats). Runs on connect and monthly on its own. Free.

_scope: write_

No parameters.
Example input: `{}`

### get_formats

The format playbook the audit wrote (not get_playbook, which is the workspace's signal playbook): per platform, every shape the workspace's posts took, ranked (winner, holding, untested, losing), with the sample each verdict rests on, the best example, the winning shape to lean on, and the cadence the history suggests. The cadence rotates winners into drafts on its own. adopt_formats schedules it.

_scope: read_

No parameters.

### adopt_formats

Schedule the playbook: set the posting plan on each platform (or the ones named) to the cadence the audit suggests, drafts waiting for a tap. Nothing publishes on its own. 1 credit per drafted post as usual.

_scope: write_

Parameters:
  - platforms: array · Limit to these platforms. Omit for every platform in the playbook.
Example input: `{"platforms":["linkedin"]}`

### list_rising

What is rising in the conversations Oetzi listens to this week: the corpus of the last seven days clustered by meaning, each cluster with its size against its usual week, which platforms, who is talking (author roles and follower tiers), five example posts, and the judge's fit score and bridge line for this workspace's buyers. Rebuilt nightly, free. Pass all: true to include clusters that did not fit. Promote one with add_topic cluster_id.

_scope: read_

Parameters:
  - all: boolean

### ask_trending

"What are people saying about X?" Pulls the closest conversations from the last two weeks of the corpus and writes a read: the threads of what is being said, who says it (by role), the tone, and one bridge line to this workspace's buyers. Returns a job_id, then get_topic_read with the read_id. Free, 10 reads a day per workspace.

_scope: read · async, returns job_id_

Parameters:
  - question (required): string
  - days: integer · 1..90 · Window in days, default 14
Example input: `{"question":"what founders say about cold email deliverability"}`

### get_topic_read

One read from ask_trending (result is null while it runs), or the last twenty reads when read_id is omitted. Free.

_scope: read_

Parameters:
  - read_id: string · uuid

### list_post_replies

Replies and comments on posts this workspace published, newest first, each with who wrote it, the text, a permalink, the drafted answer in the workspace's voice, whether it has been handled, and whether Oetzi can post the answer in place (Bluesky today, X and LinkedIn once their apps are registered). Default: only the ones still waiting. Drafts are ready as soon as a reply is found, so answering is copy + paste, or reply_to_comment.

_scope: read_

Parameters:
  - unhandled: boolean · true (default) = only replies nobody has answered or dismissed
  - post_id: string · uuid · Only replies under this published post (social_posts.id)
  - platform: string
  - since: string · date-time
  - limit: integer · 1..200
Example input: `{"unhandled":true,"limit":20}`

### get_post_reply

One reply to an own post in full: the comment, the person, the post it answers, the drafted answer, how it was handled.

_scope: read_

Parameters:
  - reply_id (required): string · uuid · Reply id from list_post_replies

### draft_post_reply

Redraft the answer to a reply on an own post: two variants in the workspace's voice with a why-this-angle note. Free. A draft under an hour old is returned as is.

_scope: write_

Parameters:
  - reply_id (required): string · uuid · Reply id from list_post_replies
Example input: `{"reply_id":"<reply id>"}`

### reply_to_comment

Answer a reply on an own post in place, under the workspace's natively connected account. Pass the text, or omit it to send the first drafted variant. Bluesky today; X and LinkedIn once their apps are registered; everywhere else this returns unsupported and the answer is copy + paste followed by mark_reply_handled. Free, requires approved: true and a post-scoped key.

_scope: post · async, returns job_id_

Parameters:
  - reply_id (required): string · uuid · Reply id from list_post_replies
  - text: string
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"reply_id":"<reply id>","approved":true,"max_credits":0}`

### mark_reply_handled

Close a reply on an own post without Oetzi posting: copied (you answered by hand on the platform) or dismissed (no answer needed). It leaves the waiting list.

_scope: write_

Parameters:
  - reply_id (required): string · uuid · Reply id from list_post_replies
  - how (required): string · one of: copied | dismissed
Example input: `{"reply_id":"<reply id>","how":"copied"}`

### refresh_post_replies

Check one published post for new replies and comments right now (replies only, free, a few seconds on Bluesky and X, up to a couple of minutes on LinkedIn). New ones get an answer drafted and appear in list_post_replies. Returns a job id.

_scope: write · async, returns job_id_

Parameters:
  - post_id (required): string · uuid · A published post (social_posts.id) or its corpus row (posts.id)
Example input: `{"post_id":"<post id>"}`

### list_presences

Where this workspace shows up, or should: every presence (a LinkedIn page per city edition, the founder's X profile, a subreddit to be useful in) with why it is recommended, its status (recommended, connected, have_it, creating, deferred, dismissed), what it is linked to, and its content plan (pillars, series, a four-week calendar) once planned. Also the publish targets available for linking.

_scope: read_

Parameters:
  - status: string · one of: recommended | connected | have_it | creating | deferred | dismissed
  - platform: string
Example input: `{"status":"recommended"}`

### discover_presences

Work out where this business should have a presence from its brain (segments, buyers, category) and what is already connected: usually 3 to 6, a page per regional or product segment on the platform where that audience lives, one hub profile, a community where the buyers ask for help. Runs once on its own after the website is read; force: true reruns it. Decisions already made are kept. Free.

_scope: write_

Parameters:
  - force: boolean · Rerun the model pass even if recommendations exist
Example input: `{"force":false}`

### set_presence

Decide one presence. connect: link it to a connected account (social_account_id) or a Postiz channel (postiz_integration_id); with neither, the only matching target on that platform is used. have_it: the workspace already has it but it is not connected yet (pass handle or url). creating: they are setting it up now. defer: put it off (days, default 30); it comes back on its own. dismiss: not for this business (reason optional). reset: back to recommended.

_scope: write_

Parameters:
  - presence_id (required): string · uuid · Presence id from list_presences
  - action (required): string · one of: connect | have_it | creating | defer | dismiss | reset
  - social_account_id: string · uuid
  - postiz_integration_id: string
  - handle: string
  - url: string · uri
  - days: integer · 1..365
  - reason: string
Example input: `{"presence_id":"<presence id>","action":"connect","postiz_integration_id":"<channel id>"}`

### plan_presence

Write (or rewrite, force: true) the content plan for one presence: 3 to 5 pillars with a share, recurring series, how it sounds here, an honest posts-per-week, and a four-week calendar of concrete working titles. The calendar feeds the cadence: each run drafts the next slot. A connected presence with a plan turns its platform's dial on at the plan's rate, still waiting for a tap. Free.

_scope: write_

Parameters:
  - presence_id (required): string · uuid · Presence id from list_presences
  - force: boolean
Example input: `{"presence_id":"<presence id>"}`

## Brain

### list_lessons

What this workspace has taught Oetzi by reacting to its work: standing rules from rejected or edited post drafts (the drafter reads these every time), notes on rendered videos, and the 👍/👎 record on matched conversations grouped by signal and by reason. Read this before drafting anything in the workspace's voice.

_scope: read_

No parameters.
Example input: `{}`

### forget_lesson

Remove one standing post rule or video note so it stops shaping future drafts. Match ratings cannot be forgotten here, re-rate the conversation with rate_lead instead.

_scope: write_

Parameters:
  - kind (required): string · one of: post_rule | video_note
  - id (required): string · uuid · The lesson id from list_lessons
Example input: `{"kind":"post_rule","id":"<lesson id>"}`

### get_competitor_report

How the market talks about each competitor in the Brain over the last 90 days: matched conversations that name them, how many were complaints, how many were hot, which platforms, three recent examples with lead ids, and the people who were in those threads (feed them to list_people or record_outreach).

_scope: read_

Parameters:
  - competitors: array · Override the Brain's competitor list for this call
Example input: `{}`

### get_knowledge_graph

The workspace as a graph: the brand node, the segments it serves, the content angles it owns, the competitors the market names next to it, and the most recently active people, each linked to the hub they engaged around (a competitor's thread, or the brand). Nodes carry portal hrefs. Use it to pick who to reach and what to say.

_scope: read_

Parameters:
  - people: integer · 0..80 · How many people to include (default 40)
Example input: `{"people":40}`

### search_knowledge

What this workspace knows about a question, freshest first: facts the team added, documents from Alexandria and news feeds, events and metrics from the connected stack. Each hit carries its source, date and url. The drafter reads the same store for every post. Free.

_scope: read_

Parameters:
  - question (required): string
  - limit: integer · 1..50
  - kinds: array
  - sources: array
Example input: `{"question":"what customers say about onboarding","limit":5}`

### add_knowledge

Tell Oetzi something it should know and cite: a fact about the product, a number, a customer story, a document. Source you, never expires unless kind is event or metric. Free.

_scope: write_

Parameters:
  - title (required): string
  - body (required): string
  - kind: string · one of: fact | document | event | metric · Default fact
  - url: string · uri
  - occurred_at: string · date-time
Example input: `{"title":"Median time to first reply","body":"Across 40 workspaces in September the median time from a match to a sent reply was 26 minutes."}`

### list_knowledge

The latest items in the store, newest first, optionally one feed's. Free.

_scope: read_

Parameters:
  - feed_id: string · uuid
  - limit: integer · 1..200

### forget_knowledge

Remove one item from the store. Free.

_scope: write_

Parameters:
  - item_id (required): string · uuid

### list_feeds

The workspace's knowledge feeds: saved Alexandria questions, news searches, and the stack's triggers and polls, with cadence, state, last run and item count. Also the five starter recipes a connected stack can turn on. Free.

_scope: read_

No parameters.

### set_feed

Create, change or remove a feed. Create a news search (query, window day|week|month), an Alexandria capability (provider, capability, options from search_alexandria), a site feed (site: the RSS or Atom url of the workspace's own site or one of its editions, with the segment it belongs to, so the cadence can post the best new article from the matching page with its coverage and related links), or a data feed (data: a JSON url the workspace runs, such as a weekly deal count per city, with its segment, read weekly by default, so the cadence drafts a market read from the numbers) with a cadence and a topic rule (always, never, or a regex pattern that makes matching items pinned topics). Change status (active, paused), cadence, label or rule on an existing feed_id. remove: true deletes it (a stack trigger is undeployed). Free.

_scope: write_

Parameters:
  - feed_id: string · uuid
  - remove: boolean
  - label: string
  - status: string · one of: active | paused
  - cadence: string · one of: hourly | daily | weekly
  - topic_rule: object
  - news: object
  - alexandria: object
  - site: object
  - data: object
Example input: `{"label":"Category news","news":{"query":"social listening tools for founders","window":"day"},"cadence":"daily","topic_rule":{"when":"never"}}`

### run_feed

Run one pull feed now instead of waiting for its cadence. Returns a job_id. Free, 30 runs a day per workspace, and an Alexandria feed whose tool has a listed price above $0.1 spends 1 credit per run.

_scope: spend · spends credits (draft) · async, returns job_id_

Parameters:
  - feed_id (required): string · uuid
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.

### search_alexandria

Discover which of Alexandria's providers and capabilities answer a question (research papers, developer docs, government records, finance, podcasts, jobs, and 90 more), or browse the catalogue by provider or category. Each tool comes with its options, listed price, and whether it costs a credit to run. Free. Run one with pull_alexandria, or save it as a feed with set_feed.

_scope: read_

Parameters:
  - question: string
  - providers: array
  - categories: array
  - limit: integer · 1..25
Example input: `{"question":"research on cold email deliverability"}`

### pull_alexandria

Run one Alexandria capability and put what comes back in the store as documents (or events). Free when the tool lists no price above $0.1 a call, else 1 credit, refunded when nothing comes back. Returns a job_id.

_scope: spend · spends credits (draft) · async, returns job_id_

Parameters:
  - provider (required): string
  - capability (required): string
  - options: object
  - item_kind: string · one of: document | event | fact
  - credits_cost: number · The tool's listed Firecrawl credits, from search_alexandria, so the estimate is exact
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.

## Voice

### list_voices

Every voice this workspace can write in: name, kind (brand writes as the company and says "we", user belongs to one person and says "I"), which is the default, how many pieces of real writing each was measured from, and where each was extracted from. Call this before drafting anything if more than one person posts from this workspace.

_scope: read_

No parameters.
Example input: `{}`

### get_voice

One voice in full: its topography document (the markdown that says what this writer ALWAYS does, OFTEN does, RARELY does and NEVER does, with the counts behind each band), the writing it was measured from, and what a person said about past drafts (sounds like me / not me). Read it before writing anything on the workspace's behalf outside draft_reply and draft_posts, which already do.

_scope: read_

Parameters:
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{}`

### list_voice_presets

The voices Oetzi ships, for a workspace with no writing to extract from yet. Each is a written topography with the same bands a measured one has. Use create_voice with a preset slug to start from one, then replace it with extract_voice the moment there is real writing.

_scope: read_

Parameters:
  - kind: string · one of: brand | user
Example input: `{"kind":"user"}`

### create_voice

Add a voice to this workspace. kind is the delineation that matters: "brand" for the company writing as itself, "user" for one person writing under their own name. Start it from a preset (list_voice_presets), from markdown you pass, or empty and then call extract_voice. The first voice a workspace has becomes its default.

_scope: write_

Parameters:
  - name (required): string · The person or company this voice belongs to
  - kind (required): string · one of: brand | user
  - owner: string · One line on whose voice this is, shown at the top of the doc
  - preset: string · A slug from list_voice_presets to start from
  - doc: string · A topography in markdown, if you already have one
  - make_default: boolean · Make this the voice drafters use when none is named
Example input: `{"name":"Max","kind":"user","preset":"plain-operator"}`

### set_voice

Replace one voice's topography with the markdown you pass (up to 8,000 chars). Every reply and post drafted in that voice afterwards reads it whole, and the Always / Often / Rarely / Never bullets are pulled out as hard rules, so keep those headings. Pass an empty string to clear it.

_scope: write_

Parameters:
  - doc (required): string · Markdown. See get_voice for the current one and the heading shape extract_voice produces.
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{"doc":"# Voice: Max\n\n## Never\n- open with a question\n- use an emoji"}`

### update_voice

Rename a voice, change whose it is, switch it between brand and user, or make it the workspace default.

_scope: write_

Parameters:
  - voice (required): string · Which voice: its slug or its id. Omit for the workspace default.
  - name: string
  - kind: string · one of: brand | user
  - owner: string
  - make_default: boolean
Example input: `{"voice":"max","make_default":true}`

### delete_voice

Remove a voice and the writing stored against it. A workspace keeps at least one, so its last voice cannot be deleted.

_scope: write_

Parameters:
  - voice (required): string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{"voice":"old-brand"}`

### extract_voice

Read somebody's actual writing and draw the voice from it. Give it profile links, feed links, bare @handles, or pasted writing. X, Bluesky, Substack, any RSS feed and any page are free. LinkedIn goes through a paid scraper because LinkedIn publishes no member-posts API, and that run costs 1 credit, refunded if under 3 usable pieces come back. Returns a job_id: poll get_job. The result replaces that voice's document, so read get_voice first if a person edited it by hand.

_scope: spend · spends credits (voice_extract) · async, returns job_id_

Parameters:
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
  - sources: array · Profile or feed addresses: "@handle", "x.com/name", "bsky.app/profile/name", "name.substack.com", "linkedin.com/in/name", or any RSS feed
  - pasted: array · Writing pasted directly
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"voice":"max","sources":["@maxg","sweetwaterlabs.substack.com"],"approved":true,"max_credits":1}`

### add_voice_samples

Add writing to a voice (posts, emails, anything they actually wrote, 20 to 2,000 chars each). Newest pieces are few-shot in every draft and up to 20 are kept. Duplicates are dropped, and the measurement behind the voice is redone. Run generate_voice_doc afterwards to redraw the document from the new evidence.

_scope: write_

Parameters:
  - samples (required): array · Texts this voice's owner wrote themselves
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{"samples":["Spent the weekend ripping out our onboarding flow. Three screens became one. Activation went from 31% to 44%. The lesson was not clever, we just deleted things."]}`

### remove_voice_sample

Drop one piece of writing from a voice by id (ids come from get_voice). The measurement behind the voice is redone.

_scope: write_

Parameters:
  - sample_id (required): string · uuid
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{"sample_id":"<sample id>"}`

### pull_voice_samples

Pull recent posts from an account this workspace has connected, into a voice. X excludes replies and reposts, Bluesky reads the public feed. For anyone else's profile, or for LinkedIn and Substack, use extract_voice. Free, no vendor cost.

_scope: write_

Parameters:
  - platform (required): string · one of: x | bluesky
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
  - limit: integer · 1..30 · How many recent posts to take (default 15)
Example input: `{"platform":"x"}`

### generate_voice_doc

Redraw a voice's topography from the writing already stored against it, plus (for a brand voice) the posts this workspace approved. Measures first, so the Never band only claims what was counted zero times. Overwrites the document, so read get_voice first if a person edited it by hand. Free, one model call.

_scope: write_

Parameters:
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{}`

### preview_voice

Write one throwaway sample in a voice, so a person (or you) can hear what it sounds like before anything real is drafted in it. Pick a context: a LinkedIn post, a post on X, a reply, a newsletter opening, a spoken video hook. Pass `doc` to hear an edited topography that has not been saved yet, which is how the voice editor tunes a document. The sample is also checked against the voice's counted rules and any it breaks come back in `violations`, which usually means the document claims something the writing never showed. Free, one model call, nothing is stored.

_scope: read_

Parameters:
  - context (required): string · Which context to write for. Call preview_voice with an unknown one to get the list back.
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
  - topic: string · What it should be about. Omit and it picks one of the workspace's own content angles.
  - doc: string · A topography to hear instead of the saved one. Use it to test an edit before set_voice writes it.
Example input: `{"context":"linkedin","voice":"max"}`

### check_voice

Check a draft against what was counted in a voice, before anyone reads it. Returns the rules it breaks: a habit this writer has in every piece and the draft does not, or one they have never once shown and the draft does. Machine-checkable rules only (punctuation, emoji, openings, person, list use), so an empty result is not a promise that the draft sounds right.

_scope: read_

Parameters:
  - text (required): string
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{"text":"Excited to announce our new feature! 🚀","voice":"max"}`

### rate_voice

Say whether a drafted reply or post sounded like the writer. not_me with a note ("too salesy", "I never say folks") becomes a standing never-rule for every later draft in that voice. me examples are shown to the drafter as register. Free.

_scope: write_

Parameters:
  - source (required): string · one of: reply | post
  - ref_id (required): string · The conversation id (optionally :variant index) or the social post id
  - verdict (required): string · one of: me | not_me
  - excerpt (required): string · The draft text being judged
  - note: string · Why, in their own words
  - voice: string · Which voice: its slug or its id. Omit for the workspace default.
Example input: `{"source":"reply","ref_id":"<conversation id>:0","verdict":"not_me","excerpt":"Great question! ...","note":"I never open with Great question"}`

## Signals

### list_signals

List this workspace's listening signals: the natural-language definitions of the conversations being watched, with id, status, cadence, and the platforms each one watches. Use the ids to filter get_leads.

_scope: read_

No parameters.

### get_signal

One signal in full: definition, status, cadence, the platforms it watches, the platform queries it compiles to, and its last runs.

_scope: read_

Parameters:
  - signal_id (required): string · uuid · Signal id from list_signals

### set_signal_status

Pause or resume a listening signal. Paused signals stop producing matches until reactivated. Draft signals cannot be activated here, they need compilation in the app first.

_scope: write_

Parameters:
  - signal_id (required): string · uuid · Signal id from list_signals
  - status (required): string · one of: active | paused

### set_signal_platforms

Choose which platforms one signal watches, e.g. everything but Reddit. Platforms left off the list stop being swept for this signal from the next sweep on; their queries stay on the plan so they can be switched back on later. Free, no sweep runs. A platform the plan never compiled queries for needs a recompile in the app first.

_scope: write_

Parameters:
  - signal_id (required): string · uuid · Signal id from list_signals
  - platforms (required): array · The platforms to watch, at least one of reddit, x, threads, linkedin, bluesky, hn, substack. Anything not listed is switched off.
Example input: `{"signal_id":"<signal id>","platforms":["x","linkedin","hn","bluesky","threads","substack"]}`

### run_signal_now

Sweep one signal now across every platform it watches, instead of waiting for its cadence. Costs 2 credits, refunded automatically when nothing qualifies. Rate limited to one manual sweep per signal every 15 minutes.

_scope: spend · spends credits (sweep) · async, returns job_id_

Parameters:
  - signal_id (required): string · uuid · Signal id from list_signals
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"signal_id":"<signal id>","approved":true,"max_credits":2}`

## Conversations

### get_leads

Fetch qualified conversations from this workspace: public posts scored for buying intent, each with the post content (truncated at 1,200 chars, use get_lead for full detail), author, platform URL, intent score (0-100), band (hot/warm/review), a why-it-matched rationale, and a ready-to-send reply draft when available. Filter by signal, status, band, audience (who is talking: in-market buyer, current customer, feature request, competitor user), or time, and sort newest (default), oldest, or by score. Conversations default to status 'new', poll with status=new to drive your loop, then mark_lead what you action.

_scope: read_

Parameters:
  - signal_id: string · uuid · Limit to one signal (see list_signals)
  - status: string · one of: new | saved | contacted | dismissed · Workflow status, 'new' = not yet actioned
  - band: string · one of: hot | warm | review · hot = strongest intent
  - audience: string · one of: in_market_buyer | current_customer | feature_request | competitor_user | other · WHO is talking: current_customer = churn save / product feedback, feature_request = roadmap input, competitor_user = switch target, in_market_buyer = market to them
  - since: string · date-time · ISO 8601 timestamp, only conversations delivered after this
  - sort: string · one of: newest | oldest | score · newest (default) | oldest | score (intent, descending)
  - limit: integer · 1..50 · Max results, 1-50 (default 20)
Example input: `{"status":"new","band":"hot","limit":10}`

### get_lead

Full detail for one conversation: untruncated post content, thread context, author profile fields, reply draft variants, enriched contact when present, and current status/feedback. Call before acting on a conversation you found via get_leads.

_scope: read_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads

### get_walk

Today's comment walk: the conversations of the last three days worth a comment, best first (fresh, an author with reach, an open question, close to the buyers, never an author we commented on this fortnight), each with the comment already written to paste under the thread (one true thing from what we know, one question at most, no link, no pitch). Free. Walk it with walk_step.

_scope: read_

Parameters:
  - limit: integer · 1..40

### walk_step

One step of the comment walk on one conversation: done (the comment was posted by hand, the conversation is contacted and the outreach recorded), skip (dismissed, with a reason that becomes a standing rule when it reads like one), later (shortlisted), or rewrite (a new comment with your note, free). Same path the text thread's "done", "skip" and "rewrite" take.

_scope: write_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads
  - action (required): string · one of: done | skip | later | rewrite
  - reason: string · For skip: why, kept on the conversation
  - note: string · For rewrite: what to change
Example input: `{"lead_id":"<lead id>","action":"done"}`

### mark_lead

Update a conversation's workflow status after acting on it: 'contacted' when outreach was sent, 'saved' to shortlist, 'dismissed' to skip. Keeps the human inbox in sync with what the agent has done.

_scope: write_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads
  - status (required): string · one of: saved | contacted | dismissed

### rate_lead

Quality feedback on a conversation, the same 👍/👎 as the human inbox, feeding the signal-tuning loop. feedback='good' confirms the match, feedback='not_relevant' marks a miss and dismisses it. Labels tune the signal's judge overnight, so rate accurately and pass a reason when one fits.

_scope: write_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads
  - feedback (required): string · one of: good | not_relevant
  - reason: string · one of: wrong_persona | wrong_topic | too_old | competitor_employee | duplicate · Why it was not relevant (only with feedback=not_relevant)

### draft_reply

Generate two reply variants for a conversation, each with a why-this-angle note, written for its platform norms and in one of the workspace's voices (pass `voice`, or let the default reply). Stored on the conversation, so the human inbox sees the same drafts. 1 credit for the first draft on a conversation, every redraft after that is free. A failed generation refunds itself.

_scope: spend · spends credits (draft)_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads
  - voice: string · Which voice replies: a slug or id from list_voices. Omit for the workspace default.
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.

### enrich_lead

Resolve a LinkedIn conversation's author to a verified contact: full name, title, seniority, company, and a verified business email (MoltSets, then Firecrawl's LinkedIn enrichment). Only works for LinkedIn matches with a personal profile. 1 credit when something comes back, free on a miss, cached afterwards (repeat calls cost nothing). Same as resolve_person, addressed by conversation.

_scope: spend · spends credits (resolve)_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"lead_id":"<lead id>","approved":true,"max_credits":1}`

### translate_post

Render a non-English conversation in English: the post body and its thread context, tone and handles preserved. Free. Made once per post and cached, so repeat calls return instantly. get_lead reports lang, call this when it is not 'en'.

_scope: read_

Parameters:
  - lead_id (required): string · uuid · Conversation id from get_leads
Example input: `{"lead_id":"<lead id>"}`

## People

### lookup_linkedin

Any LinkedIn URL to what is known about it, through Firecrawl's LinkedIn enrichment (the URL is routed to the team's Alexandria providers: Apollo, FullEnrich, Data Legion). A profile (/in/<slug>) becomes a person in the workspace's graph with a resolved contact (name, title, company, business email when a provider has it): the same lookup as resolve_person, by URL. A company page (/company/<slug>) becomes a company record (name, website, industry, size, founded, location, description), kept in the knowledge store as a fact so drafts and Ask know it. 1 credit when something comes back, free on a miss, cached afterwards. If Firecrawl says enrichment is not enabled or a provider's terms are not accepted, the result carries the settings link.

_scope: spend · spends credits (resolve)_

Parameters:
  - url (required): string · https://www.linkedin.com/in/<slug> or /company/<slug>
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"url":"https://www.linkedin.com/company/firecrawl","approved":true,"max_credits":1}`

### list_people

People who interacted with posts this workspace matched: the authors of conversations and, once engager pulls run, the people who liked, reposted, replied to, or quoted them. Filter by a conversation (lead_id), interaction kind, platform, follower floor, whether a verified contact exists, or a tag. Sorted by most recent interaction.

_scope: read_

Parameters:
  - lead_id: string · uuid · Only people who touched the post behind this conversation
  - post_id: string · uuid · Only people who touched this post (posts.id)
  - kind: string · one of: authored | liked | reposted | replied | commented | quoted
  - platform: string · x | reddit | linkedin | threads | bluesky | hn | substack
  - min_followers: integer · 0..9007199254740991
  - has_contact: boolean · true = has a verified email or phone in this workspace
  - tag: string
  - since: string · date-time · Interactions recorded after this ISO timestamp
  - limit: integer · 1..100
Example input: `{"kind":"authored","min_followers":500,"limit":20}`

### get_person

One person in full: every platform identity, this workspace's interactions with them (each with the post and the conversation it became), verified contacts, tags, and every outreach recorded against them. Check outreach before contacting anyone.

_scope: read_

Parameters:
  - person_id (required): string · uuid · Person id from list_people or a conversation's person_id

### resolve_person

Resolve a person to a verified contact (name, title, company, business email, mobile on premium) through the vendor waterfall: MoltSets, then Firecrawl's LinkedIn enrichment (Apollo, FullEnrich, Data Legion through Alexandria). Needs a personal LinkedIn identity. 1 credit when something comes back, free on a miss, cached afterwards.

_scope: spend · spends credits (resolve)_

Parameters:
  - person_id (required): string · uuid · Person id from list_people or a conversation's person_id
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"person_id":"<person id>","approved":true,"max_credits":1}`

### tag_person

Add or remove free-text tags on a person for this workspace (for example "champion", "do-not-contact", "q4-list"). Tags filter list_people.

_scope: write_

Parameters:
  - person_id (required): string · uuid · Person id from list_people or a conversation's person_id
  - add: array
  - remove: array
Example input: `{"person_id":"<person id>","add":["champion"]}`

### record_outreach

Report that you contacted a person: the channel, what you sent, the outcome, and who you are. This is the handoff that keeps the graph honest, so nobody in the workspace double-contacts. Marks the linked conversation contacted when lead_id is given, and emits outreach.recorded.

_scope: write_

Parameters:
  - person_id (required): string · uuid · Person id from list_people or a conversation's person_id
  - channel (required): string · x_reply | x_dm | linkedin_dm | linkedin_comment | email | imessage | call | other
  - message: string · What was sent, verbatim or summarized
  - outcome: string · sent | replied | booked | declined | bounced | no_answer
  - agent: string · Who did it: your agent name, or a person
  - external_ref: string · Vendor message id or permalink
  - lead_id: string · uuid · The conversation this outreach answers, if any
  - metadata: object
Example input: `{"person_id":"<person id>","channel":"x_reply","outcome":"sent","agent":"claude-code","lead_id":"<lead id>"}`

### pull_engagers

Pull the people who engaged with a post (liked, reposted, quoted, replied, commented) into the people graph, then read them with list_people. Address the post by conversation (lead_id), post id, or its exact URL. Works on X, Bluesky, LinkedIn, Reddit, and Hacker News. 1 credit per post, refunded when fewer than 3 people come back, free when already pulled.

_scope: spend · spends credits (engagers) · async, returns job_id_

Parameters:
  - lead_id: string · uuid
  - post_id: string · uuid
  - url: string · uri · Exact URL of a post Oetzi has already seen
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"lead_id":"<lead id>","approved":true,"max_credits":1}`

## Share of model

### list_geo_questions

The buying questions this workspace tracks across AI engines (ChatGPT, Gemini, Perplexity, Grok, Google AI Mode), as a user would type them.

_scope: read_

No parameters.

### get_share_of_model

The latest share-of-model board: for every tracked question, which engines mentioned this workspace, at what rank, which competitors they named instead, and the sources they cited. Includes the current content plan (what to publish to close the gaps) when one has been drafted.

_scope: read_

No parameters.

### run_geo_check

Ask every configured AI engine each tracked question now and record who they recommend. Costs 2 credits per active question. Rate limited to one manual board per hour.

_scope: spend · spends credits (geo) · async, returns job_id_

Parameters:
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"approved":true,"max_credits":10}`

## Content

### list_content

Short-form video ideas, drafts, renders and carousels in this workspace. idea = proposed, waiting for approve (render_content) or reject (reject_content). rendered = awaiting review, accepted = approved, posted. Rendered items carry download URLs, ideas carry their outline, source and thumbnail.

_scope: read_

Parameters:
  - status: string · one of: idea | draft | rendered | accepted | rejected | superseded | archived | posted
  - limit: integer · 1..50

### generate_content

Propose short-form video ideas from what the market is saying: this workspace's hottest matches by default, or one conversation (lead_id), one signal (signal_id), posts that already earned attention (proven), or a brief. Each idea is a hook, a three-beat outline, its source and a thumbnail, waiting in the review deck. 1 credit per batch of up to 5, re-rolling the same source is free, nothing is rendered until render_content approves an idea and picks its length.

_scope: spend · spends credits (video_idea) · async, returns job_id_

Parameters:
  - count: integer · 1..5 · How many ideas, 1-5 (default 3)
  - brief: string · A topic or request the whole batch must be about
  - lead_id: string · Make ideas about this one conversation (from get_leads)
  - signal_id: string · Make ideas from this signal's recent matches
  - proven: boolean · Make ideas that read out posts which already earned attention, with credit
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"count":3,"approved":true,"max_credits":1}`

### render_content

Approve an idea and render it at a length. standard_30 is 1 credit (voice, captions, a generated scene, up to 30 s), standard_60 is 2 credits (up to 60 s). Carousels are always 1 credit. Persona tiers are listed but not renderable yet. Also re-renders a draft whose render failed. Failed renders are free, a retry of the same post is never charged twice.

_scope: spend · spends credits (video) · async, returns job_id_

Parameters:
  - post_id (required): string · An idea (from list_content status idea) or a draft
  - tier: string · one of: standard_30 | standard_60 | persona_5 | persona_10 | persona_15 · standard_30 (default) or standard_60
  - hook_variants: integer · 1..2 · 2 = also render a second hook over the same body as an A/B pair (two renders, twice the credits)
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"post_id":"<idea id>","tier":"standard_30","approved":true,"max_credits":1}`

### reject_content

Reject an idea before anything is rendered. A reason becomes a standing rule the planner obeys in every later batch. Free.

_scope: write_

Parameters:
  - post_id (required): string
  - reason: string · Why, in one line. Becomes a rule for future ideas
Example input: `{"post_id":"<idea id>","reason":"never lead with a competitor's name"}`

### revise_content

Revise content with feedback instead of accepting or rejecting it. An idea is rewritten in place (hook, beats, slide copy, scene) before anything renders: free, synchronous, the revised idea comes back. A rendered video is forked with the feedback applied and the fork is rendered: 1 credit, charged when the render lands, the original is kept as the fork's parent. Either way the feedback becomes a standing rule for later batches.

_scope: spend · spends credits (video) · async, returns job_id_

Parameters:
  - post_id (required): string · An idea or a rendered video (from list_content)
  - feedback (required): string · What should change, in plain words
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"post_id":"<idea id>","feedback":"open on the founder's own words, not the stat","approved":true,"max_credits":1}`

### get_video_performance

How the video loop is doing, by template arm: proposed, rejected, rendered, accepted and published counts over the window, hook pairs, the hooks that went live, and the benchmark (first 20 published videos across the arms). Reach numbers join in once the metrics pull covers video.

_scope: read_

Parameters:
  - days: integer · 1..90 · Window, default 30

### create_persona

Start the workspace's persona: four portrait candidates of one synthetic creator, generated from a look you describe (or from the brain when you don't). Free. Pick one with choose_persona. The persona is fully generated, exclusive to this workspace, and speaks every persona-tier video.

_scope: write_

Parameters:
  - look: string · Who the creator is, in a sentence: age range, style, setting. Never a real person.
Example input: `{"look":"a thirty-something founder who runs a small SaaS, hoodie, home office"}`

### choose_persona

Make one portrait candidate the workspace's persona, with a name and an ElevenLabs voice. Unlocks the persona lengths on render_content (5 s hook = 3 credits, 10 s = 4, 15 s = 6). Free.

_scope: write_

Parameters:
  - asset_id (required): string · One candidate from create_persona
  - name (required): string
  - voice (required): string · one of: Rachel | Brian | Jessica | Liam | Laura | Chris
  - tone: string · One line the script writer keeps in mind, e.g. 'dry, quick, a little tired'
Example input: `{"asset_id":"<candidate id>","name":"Sam","voice":"Laura","tone":"warm and quick"}`

### create_loop_kit

Make the workspace's background loop kit: three short generated high-speed loops (an endless runner, a first-person corridor, a marble run, described generically, never a real game) that the sound-off and split-screen formats cut from. Free, made once, about a minute a loop. Until it exists those formats run over a shared ambient clip.

_scope: write · async, returns job_id_

No parameters.
Example input: `{}`

### recut_demo

Cut a finished demo again without recording: another wallpaper, the presenter bubble on or off. Uses the kept recording, events, narration and voice; the 16:9 cut, the silent clip and the 9:16 follow crop are all redrawn. 1 credit, plus the presenter difference when the demo never had one rendered.

_scope: spend · spends credits (demo_recut) · async, returns job_id_

Parameters:
  - demo_id (required): string
  - wallpaper: string · one of: aurora | dusk | mint | graphite | paper | ember | sunrise | ocean
  - presenter: boolean
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"demo_id":"00000000-0000-0000-0000-000000000000","wallpaper":"ember"}`

### create_demo

Start a product demo video from a docs page and a demo login. The agent reads the docs and DECLARES what it understood, the exact click path it will take, and any questions, then waits. Nothing is recorded and nothing is charged until confirm_demo. Returns a job to wait on, then get_demo shows the declaration.

_scope: write · async, returns job_id_

Parameters:
  - docs_url (required): string · The product's documentation page; the agent reads it and the pages it links to
  - app_url (required): string · Where the recording starts: the app's login page or home
  - brief: string · What the demo should show: one feature, a launch, or the whole product end to end
  - title: string
  - login: object · A DEMO account only. Stored encrypted, never shown to the model; the runner types it where the model points.
  - wallpaper: string · one of: aurora | dusk | mint | graphite | paper | ember | sunrise | ocean · Background behind the window in the final cut, default aurora
  - presenter: boolean · A presenter in a round bubble speaks the whole take (the workspace's replica or the stock face). Prices the demo at 5 credits instead of 3.
Example input: `{"docs_url":"https://docs.example.com","app_url":"https://app.example.com/login","brief":"Show how a team lead invites a teammate and assigns them a project","login":{"username":"demo@example.com","password":"<demo password>"}}`

### get_demo

One demo session: the agent's declaration (what it understood, its click plan step by step, open questions), the action log while it records, and the video when done.

_scope: read_

Parameters:
  - demo_id (required): string

### list_demos

Demo sessions in this workspace, newest first, with status and video links.

_scope: read_

Parameters:
  - limit: integer · 1..50

### answer_demo

Answer the questions the agent asked in its declaration (or add facts it should know). The agent re-reads and re-declares; wait_job then get_demo again. Also the way to change the brief on a declared plan.

_scope: write · async, returns job_id_

Parameters:
  - demo_id (required): string
  - answers: object · question text -> answer
  - brief: string · A new brief, replaces the old one

### confirm_demo

Confirm the declared plan and record. 3 credits, charged now and refunded if the recording fails. Refused while questions are open unless ignore_questions is true. The recording, narration and render take 5 to 12 minutes; wait_job, then get_demo for the video.

_scope: spend · spends credits (demo_render) · async, returns job_id_

Parameters:
  - demo_id (required): string
  - ignore_questions: boolean · Record even with unanswered questions, the agent uses its best judgement
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"demo_id":"<demo id>","approved":true,"max_credits":3}`

### cancel_demo

Cancel a demo that has not finished. A recording in flight stops at its next step.

_scope: write_

Parameters:
  - demo_id (required): string

### list_topics

The Topics list: what this workspace has decided to post about. Things the team added (source you: a launch, a number, something that happened) sit pinned first, then trends, rising clusters and reads promoted from Trending. Each row says which platforms already drafted from it. The posting plan drafts open topics before anything else. Free.

_scope: read_

Parameters:
  - status: string · one of: open | done | all · Default open

### add_topic

Put something on the Topics list. Either your own news (title, optional detail and url: 'we shipped X', 'crossed 1,000 users') which pins to the top and drafts first, or a promotion from Trending by trend_key (from list_trends), cluster_id (from list_rising) or read_id (from ask_trending). Free. Drafting from it is draft_posts with topic_id, one credit per platform run.

_scope: write_

Parameters:
  - title: string · Your own news, in a sentence
  - detail: string · What changed, what it lets people do, the number
  - url: string · uri
  - trend_key: string · Promote a pick from today's scan (list_trends)
  - cluster_id: string · uuid · Promote a rising cluster (list_rising)
  - read_id: string · uuid · Promote a read (ask_trending)
Example input: `{"title":"We shipped per-signal platform picks","detail":"Each signal now chooses which of the seven platforms it watches. Same sweep price."}`

### set_topic_status

Mark a topic done (it stops drafting), reopen it, or remove it from the list. Free.

_scope: write_

Parameters:
  - topic_id (required): string · uuid
  - status (required): string · one of: open | done | removed

## Outcomes

### get_outcomes

What Oetzi's work got this workspace: clicks on tracked links, replies, signups, and customers, each tied to the conversation or post that earned it. Returns the 7-day (or since) totals, credits spent this period against the wins in it (credits_per_signup), the recent rows, and the tracked links with their click counts. Call this before reporting on results.

_scope: read_

Parameters:
  - since_days: integer · 1..365 · Window for the totals (default 7)
  - kind: string · one of: click | reply | signup | customer · Only rows of this kind
  - limit: integer · 1..500
Example input: `{"since_days":30}`

### record_outcome

Record a win against the thing that earned it: kind 'signup', 'customer', or 'reply', attached to a conversation (lead_id), a published post (post_id), or a tracked link (link_code). Pass value_usd when a customer has a price. Use this when your CRM, Stripe, or the founder tells you a conversation Oetzi found turned into a user, so the ledger and the home page can price the month honestly. Free.

_scope: write_

Parameters:
  - kind (required): string · one of: reply | signup | customer
  - lead_id: string · uuid · The conversation that earned it
  - post_id: string · uuid · The published post (social_posts.id) that earned it
  - link_code: string · The tracked link code the person came through
  - value_usd: number · 0..1000000
  - note: string · Who, and how you know: 'signed up as jane@acme.com from the r/SaaS reply'
  - occurred_at: string · date-time
Example input: `{"kind":"signup","lead_id":"8c83…","note":"signed up the same day from the reply"}`

## Plays

### list_plays

The plays library: every named tactic Oetzi can run for this workspace, each with why it works, what Oetzi does, what the human taps, what one run costs, what to expect, and where it stands right now (ready, running with a receipt, done, blocked with the reason, or coming). `proposed` is the top three by fit. Call this before suggesting what to do next.

_scope: read_

No parameters.
Example input: `{}`

### run_play

Run one play from list_plays. Listening plays compile and launch their signal (a launch sweep, refunded if nothing qualifies) and return a job_id. The citation play starts a share-of-model check. The posting play turns on a three-a-week plan and drafts the first post. The comment and presence plays are free. A play already running is left alone and the note says where its work is.

_scope: spend · spends credits (sweep)_

Parameters:
  - play_id (required): string · one of: answer-the-thread | competitor-complaints | citation-gap | post-from-the-thread | answer-every-comment | show-up-where-they-gather | alternatives-page | skill-that-cites-you | github-listening
  - dry_run: boolean · true = estimate only, nothing runs and nothing is charged
  - approved: boolean · Required to run a capability that spends credits. Omit it to get a dry-run estimate first.
  - max_credits: number · Ceiling for this call. The call fails with budget_exceeded when the estimate is higher.
Example input: `{"play_id":"answer-the-thread","approved":true,"max_credits":2}`

## Jobs

### get_job

State of a background job: status (running | done | error), phase (queued | running | done | error), progress when the worker reports it, the result once done, and the error otherwise.

_scope: read_

Parameters:
  - job_id (required): string · uuid · job_id returned by an async capability

### wait_job

Block until a job finishes, up to 25 seconds per call, then return its state. Call it again while status is still running. Cheaper than polling get_job in a loop.

_scope: read_

Parameters:
  - job_id (required): string · uuid · job_id returned by an async capability
  - timeout_ms: integer · 1000..25000 · Default and maximum 25000

## Events

### list_events

Events in this workspace after a cursor: conversations delivered (lead.delivered, lead.hot), people discovered, posts published, share-of-model checks, jobs completed, outreach recorded. Returns up to 200 in id order. Start with since=0, then pass the last id you saw. Call with no since to get the current cursor.

_scope: read_

Parameters:
  - since: integer · 0..9007199254740991 · Exclusive cursor (event id). Omit to learn the latest id.
  - types: array · Any of: lead.delivered, lead.hot, person.discovered, post.published, knowledge.item, reply.received, geo.check.completed, job.completed, outreach.recorded, outcome.recorded
  - limit: integer · 1..200
Example input: `{"since":0,"types":["lead.hot"],"limit":50}`

### wait_events

Block until at least one event after the cursor lands, up to 25 seconds per call, then return the batch. Loop on it for a push-style agent without running a webhook endpoint.

_scope: read_

Parameters:
  - since (required): integer · 0..9007199254740991 · Exclusive cursor (event id)
  - types: array · Any of: lead.delivered, lead.hot, person.discovered, post.published, knowledge.item, reply.received, geo.check.completed, job.completed, outreach.recorded, outcome.recorded
  - limit: integer · 1..200
  - timeout_ms: integer · 1000..25000
Example input: `{"since":120,"types":["lead.hot","person.discovered"]}`

### list_webhooks

Webhook subscriptions in this workspace: URL, event types, active flag, last status, failure count. Secrets are never returned after creation.

_scope: read_

No parameters.

### subscribe_webhook

Register an https URL to receive events as signed JSON POSTs (header X-Oetzi-Signature = sha256 HMAC of the body with the returned secret). Pass event_types or ["*"] for everything. A subscription is disabled after 20 consecutive failures. The secret is returned once.

_scope: write_

Parameters:
  - url (required): string · uri
  - event_types (required): array
  - label: string
Example input: `{"url":"https://example.com/oetzi","event_types":["lead.hot","person.discovered"]}`

### unsubscribe_webhook

Remove a webhook subscription by id.

_scope: write_

Parameters:
  - id (required): string · uuid
