campaigns — full tool reference
Post calendar, human review queue, account-level post performance analytics and the Zernio account to BU/owner mapping.
| Endpoint | https://campaigns.mcp.devfellowship.com/mcp |
| Package | packages/dfl-mcp-campaigns |
| Tools | 34 |
| Tool | Description |
|---|---|
list_post_business_units | The business units the post calendar can schedule for — strategy.business_units, archived ones excluded. Call this FIRST: every other tool takes a business_unit_id (a uuid), never a name like “itera”. |
list_zernio_profiles | Who publishes where: each Zernio profile (a person or the company) with its connected accounts per network, plus the accounts that belong to no profile. Call this FIRST when drafting a post: draft_post_for_review requires zernio_profile_id — the profile of the person or brand the post goes out as (“Criador” in the dfl-campaigns UI). A post is NOT limited to that profile’s own accounts — the app allows channels across profiles (e.g. the brand DevFellowship plus a team member’s personal accounts); the profile just names who the post is mainly for. |
list_zernio_accounts | The social accounts connected in Zernio, each with the id a channel needs to actually publish. Call this BEFORE draft_post_for_review whenever the post has channels: Zernio publishes per ACCOUNT, and there is more than one account on the same platform (a company profile and a personal one), so “instagram” alone does not say where the post goes out. A channel drafted without its zernio_account_id is dropped at dispatch, and the review queue cannot add the account afterwards — that post has to be redone. |
list_posts | Read the post calendar, ordered by scheduled_for. Without business_unit_id it is the consolidated view across every BU. Pass status: “awaiting_review” to read the human review queue — everything the AI wrote that is still waiting on a person. assignee_id and zernio_profile_id narrow it to one person or one profile; while those columns do not exist yet the filter is skipped and notes says so. Each post carries assignment; pass include_metrics: true to attach latest_metrics per platform and account. Each post carries archetype and media_group (its content-format tag; null when unset). |
list_core_daily_topics | What each person said in the last N days, merged per person from the Discord channel #core-daily-updates (primary) and the Core Daily meeting transcripts (when there was one). Each line has source (“channel” with a Discord url, or “meeting”); lines under 8 words and repeats are removed, at most 30 per person, newest first. sources says which side answered. Read-only. Use it to propose one draft per person: each line is raw speech, so rewrite it into a hook before calling draft_post_for_review, and pass assignee_id = user_id when it is present (user_id is only set on an exact name match with a member). |
list_suggested_topics | Read the suggested topics on the Curadoria board of /posts/pautas in dfl-campaigns. A suggested topic (a curated content topic, “pauta” in Portuguese) is an idea for content that a person or an agent put on the board and that later turns into one or many posts. Each topic has a title, briefing, status (idea, todo, awaiting_content, in_production, awaiting_approval, posted, standby), optional due_date, formats, channels, reference_links, business_unit_id, assignee_id and a url to its card. Active topics are returned by default; pass archived=true for the archived ones. Filters are applied after the read. Read-only. Call it before create_suggested_topic so you do not add a duplicate. If the answer says the board is not live on this deployment, do not retry. |
create_suggested_topic | Add one suggested topic to the Curadoria board of /posts/pautas in dfl-campaigns. A suggested topic (a curated content topic, “pauta” in Portuguese) is an idea for content that a person or an agent proposes and that later turns into one or many posts. Creating one posts NOTHING and drafts no post: it only puts a card on the board, marked as created by an agent (origin “mcp”), for a person to pick up. To draft a post use draft_post_for_review. The status defaults to idea. Run list_suggested_topics first to avoid duplicates. The answer carries the card url. A refusal (not available on this deployment, a rule, a missing BU or user) says do not retry; only a temporary failure may be retried. |
update_suggested_topic | Edit the content of one suggested topic (“pauta”) on the Curadoria board of /posts/pautas in dfl-campaigns: title, briefing, due date, formats, channels, reference links, BU or assignee. Any member can edit any topic. Send only the fields to change; an omitted field keeps its value, and null clears briefing, due_date, business_unit_id or assignee_id. Arrays (formats, channels, reference_links) replace the whole list. It does NOT change the column or the order: use move_suggested_topic for that, and archive_suggested_topic to archive. It refuses an empty change, an unknown field and an unknown id. Get the id from list_suggested_topics. A refusal says do not retry; only a temporary failure may be retried. |
move_suggested_topic | Move one suggested topic (“pauta”) to a column of the Curadoria board of /posts/pautas in dfl-campaigns and, optionally, to a place inside it. status is the target column (it may be the current one, to only reorder). Without before_id/after_id the card goes to the end of the column; with before_id it lands right above that card, with after_id right below it (send one, never both). The neighbour must be an active topic already in the target column. Any member can move any topic. It does not edit content (update_suggested_topic) and does not touch posts. A refusal says do not retry; only a temporary failure may be retried. |
archive_suggested_topic | Archive one suggested topic (“pauta”) of the Curadoria board of /posts/pautas in dfl-campaigns, or bring an archived one back with restore=true. Archiving hides the card from the board but keeps it, and it is reversible: this is the safe way to take a topic off the board. Any member can archive or restore any topic. To remove one for good use delete_suggested_topic. Archived topics are read with list_suggested_topics archived=true. A refusal (unknown id, not available on this deployment) says do not retry; only a temporary failure may be retried. |
delete_suggested_topic | Permanently delete one suggested topic (“pauta”) from the Curadoria board of /posts/pautas in dfl-campaigns. It cannot be undone. Only the creator of the topic or an admin may delete it: the server decides that, from the caller session, and refuses everybody else with “do not retry” (ask the creator or an admin instead). To take a topic off the board without losing it, use archive_suggested_topic. Pass confirm_title equal to the topic’s current title, exactly as list_suggested_topics shows it; a different title, or an id that is not on the board, deletes nothing. Posts already drafted from the topic are not deleted. |
get_post | One post with its channels, its lifecycle state and its approval trail — who approved it and when, and whether it was already dispatched to Zernio. Also returns assignment (zernio_profile_id, assignee_id; null while those columns do not exist) and latest_metrics: the latest views/likes/comments per platform AND Zernio account, never summed across platforms. The post always carries archetype and media_group (its content-format tag; null when unset or while the columns do not exist). live is { published_at, published_url } once dfl-campaigns read the post LIVE in Zernio (status published): the link to the post on the network. null while it is not known live. Use it to answer “how did this post do” and “where is it”. |
draft_post_for_review | Write one post into the calendar. It lands in awaiting_review and NOTHING leaves this server until a person opens the queue in dfl-campaigns, reads it and approves it — you cannot clear it yourself; approve_post only records a decision a person already gave you. Schedule the time you want it to go out; a channel only reaches its network if it carries the connected zernio_account_id. An Instagram Reel cover is optional: pass cover_media_id (the COVER_MEDIA_ID from the thumbify-reel-cover skill) or cover_url (a Thumbify render URL), or Instagram uses frame 0 of the video. A cover that is not a Thumbify cover is refused at approval. The Lesson Studio cover slide is NOT carried over. A carousel (Instagram, or a TikTok photo carousel) is either media_ids (existing public.media ids) or media_urls (rendered images, e.g. from render_composition_images; dfl-campaigns registers them as your media). A single image is media_id; TikTok accepts it as a photo post (JPEG, PNG or WebP, up to 20 MB each). |
revise_post_in_review | Rewrite a post that is still in the queue — typically after a reviewer left review_notes, or when a rejected post is being redone. Only draft, awaiting_review and rejected posts can be revised: once a person has approved it, the text they read is the text that goes out. Status is never changed here. |
approve_post | Record that a PERSON cleared this post for publishing, when they said so to you instead of clicking in the dfl-campaigns UI — the case the queue had no answer for (Tainan asked for a Reel over Telegram, 2026-09-11). It does not approve on your behalf: it WRITES DOWN a decision a human already took, and approval_reference is where you point at the message they took it in. Never call this off your own judgement and never invent a reference to satisfy the field — no server can tell a made-up pointer from a real one, which is precisely why the queue exists. If nobody told you to publish, leave the post in awaiting_review and say it is waiting on a person. Restricted to super admins; everybody else is refused and has to approve in the UI. Approving IS publishing: the same call sends the post to Zernio, scheduled for its scheduled_for (a time that already passed goes out now), and Zernio publishes it then. If Zernio refuses, this call fails and the post stays approved but NOT scheduled — fix what the error names and retry with dispatch_post. If the error says it cannot tell whether Zernio received the post, do NOT retry: tell the person to check Zernio. |
dispatch_post | Approving a post already sends it to Zernio — you do not need this after approve_post or after a person approves in the UI. Use it only to RETRY a post that is approved but not scheduled, because Zernio refused it at approval time (the approval error said so). Fix what that error named first, or the retry fails the same way. The server refuses anything that is not approved with a recorded approver, so this can never publish something nobody cleared. A post whose send outcome is unknown stays dispatched without a Zernio id and is refused here on purpose, so it is never published twice — tell the person to check Zernio. Zernio publishes at scheduled_for; a time that already passed goes out now, and the calendar is updated to match. |
unschedule_post | Stop a post that Zernio is holding from publishing: the server cancels it in Zernio FIRST and only then rewrites the row, which comes back as awaiting_review with the approval trail cleared. It is NOT a rejection and NOT a shortcut around the queue — it only moves a post BACKWARDS, into human review, and someone has to approve it again for it to be scheduled at all. Judging the post is still not something this server does. A new scheduled_for is required because it is what the post holds while it waits (and it has to be in the future). This only works while Zernio still HOLDS the post: once it published — which includes the last few minutes before scheduled_for, when Zernio may already be sending — nothing here takes it back, and the call is refused saying so rather than reporting a cancel that did not happen. Deleting what is already on the network is done in the network itself, by a person. |
reject_post | Move a post that YOU created from awaiting_review to rejected — for example a smoke or test post, or a post the person told you to drop. It refuses any post created by somebody else: rejecting another person’s post is a judgement on content, and a person does that in the dfl-campaigns UI. It also refuses a draft (delete it with delete_post), an approved post, and a scheduled or published post (unschedule_post takes a scheduled post back to the queue). The reason is stored as the post’s review_notes. A rejected post never publishes; delete_post then removes it. |
delete_post | Permanently delete a post that YOU created, while it is a draft or rejected — for example a smoke or test post. The post and its channels go; this cannot be undone. It refuses a post created by somebody else, a post in the review queue (reject it with reject_post first), and every approved, scheduled or published post. |
archive_post | Hide an approved or PUBLISHED post from dfl-campaigns — the calendar, the profile pages (recent posts, top posts, cadence, counts) and analytics — while KEEPING its metric snapshots in the database. Use it for a published post that should not be in the lists, for example a test post that already went live. It is a soft delete that records who archived the post and why; it does not delete anything and it does not touch Zernio or the social network (the post stays live there, or has already expired). There is no un-archive tool. Restricted to global admins (IAM level 80 or higher): everybody else gets 403. It refuses a draft or rejected post (use delete_post), a post in the review queue (reject_post, then delete_post), and a post still scheduled in Zernio (unschedule_post first). Only call it when a person asked for the post to go, and put where they asked in the reason. |
set_post_keywords | Replace the SEO keywords linked to a post, in any status — including posts already dispatched, so older content can be mapped to keywords. It never changes the post itself and never touches its approval. The first keyword is the primary one. |
set_post_content_tag | Write media_group + archetype on ONE post, in any status — published posts included. It writes those two fields and nothing else: title, text, media, schedule and approval stay as they are. The creator of a post may tag it; any other post needs a global admin (IAM level >= 80). The pair must match the content-visual taxonomy (e.g. vertical-short + talking-head; archetype null is a media-group-only tag). Pass dry_run: true to see the before/after and the authority check without writing. |
draft_story_sequence_for_review | Write an ordered Instagram Story sequence: one post per video part, published in index order a few minutes apart. Get media_ids from the studio tool split_export_for_stories, and pass them IN THAT ORDER — the order of the list is the order on Instagram. A single Story (one video of 3 to 60 s) is a sequence of 1: pass one media id. Instagram only, exactly one account. Every part lands in awaiting_review and NOTHING is published until a person approves the sequence (in the dfl-campaigns UI, or through approve_story_sequence with the reference of the message where the person said so). Optional user_tags: [{username, x?, y?}], up to 3 (our limit). It tags other Instagram accounts on the Stories. One list for the whole sequence: it is stored on every part and sent to Zernio as platformSpecificData.userTags, on Stories only. A leading ”@” is stripped and names are lowercased; x and y (0.0 to 1.0) come together or not at all. Zernio (docs.zernio.com/platforms/instagram): “Images require x/y (0.0 to 1.0); Reels and videos ignore coordinates; Stories take them optionally.” The tagged account must be a public Business or Creator account. WHAT INSTAGRAM RENDERS ON A STORY WAS NOT VERIFIED: it may be a plain tag and not the interactive mention sticker. Do not promise the person a clickable mention. The tags are fixed at draft time and are shown on the review page and in get_story_sequence (sequence.user_tags, parts[].user_tags). Until the dfl-schema migration that adds campaigns.posts.story_user_tags is applied, a call with user_tags answers 503 story_sequence_schema_missing and creates nothing; a call without user_tags works as before. Returns the sequence and review_url: send that link to the person who reviews it. |
get_story_sequence | One Story sequence with every part in index order: status, scheduled_for, updated_at, the approval trail and the Zernio id, and the user tags (sequence.user_tags and parts[].user_tags; absent when the dfl-schema column story_user_tags is not applied yet). Read it before approve_story_sequence, and show the person what they approve. |
approve_story_sequence | Record that a PERSON cleared this whole Story sequence for publishing (show them its user_tags first: the tags go to Instagram with the Stories, and what Instagram renders was not verified), when they said so to you instead of clicking in the dfl-campaigns UI. It does not approve on your behalf: it WRITES DOWN a decision a human already took, and approval_reference is where you point at the message they took it in. Call it ONLY with an explicit human approval and the reference of that message. Never call this off your own judgement and never invent a reference to satisfy the field — no server can tell a made-up pointer from a real one. If nobody told you to publish, leave the sequence in awaiting_review and say it is waiting on a person. Restricted to super admins; everybody else is refused and has to approve in the UI. Approving IS publishing: the same call sends the parts to Zernio as Stories, in index order. If one part fails, the later parts are not sent (the order is kept); the answer says which part and what to do. If it says nobody can tell whether Zernio received a part, do NOT retry: tell the person to check Zernio. |
check_story_sequence_order | Read from Zernio when each part of the sequence was published, and compare the order. check.ok is true only when every checked part went out after the part before it. check.violations names the parts out of order; check.incomplete names the parts that are not published yet (check again later). parts[].outcome: ok = live (publishedUrl is the live link), pending = Zernio holds it, not_sent = never sent (post_status says approved or awaiting_review; it is not a refusal), rejected = Zernio refused it or reports it failed, unknown = check Zernio. It sends nothing. It records each part that Zernio reports live as published, with its live URL. |
retry_story_sequence | Use it only after approve_story_sequence (or a later retry) stopped at a part that Zernio rejected or refused. It sends the parts that are approved but not scheduled, in index order. It never approves anything: the server refuses a part that no person cleared. action “none” means there was nothing to send. A 409 retry_stopped means the server will not retry — for example a part whose send outcome is unknown. Then tell the person to check Zernio. |
get_business_unit_voice | READ the brand VOICE of a business unit before you write any post copy — the writing patterns that say how that BU sounds. Source is strategy.writing_patterns, the single canonical store, read through the strategy MCP tool list_writing_patterns with your own JWT. Call it after list_post_business_units and BEFORE draft_post_for_review: a Reel written without it is written in a voice you guessed. Each row is one “slot” (e.g. “book”, “youtube”); the free-form pattern jsonb carries description, toneAxes, vocabularyDo / vocabularyAvoid and examplePairs. Optionally narrow to one slot. This tool is READ-ONLY: authoring a voice happens in BM Canvas or on the strategy MCP. |
list_campaign_accounts | List each platform and Zernio account combination that has stored post metric snapshots. Accounts stay separate even when they use the same platform. An RLS-safe database RPC excludes soft-deleted posts before it computes the summaries. |
rank_account_posts | Rank one Zernio account’s campaigns posts. lifetime uses the latest absolute counter. period_gain subtracts the baseline from the latest observation inside the requested period and can be negative. The RLS-safe database RPC computes and bounds the ranking. |
get_post_metric_history | Return stored daily absolute counters for one campaigns post. Account and platform targets are required so separate publication targets are never combined. Reads fail explicitly above 10,000 visible snapshots; use a smaller date range. |
get_account_analytics | The last 30 São Paulo days of ONE Zernio account on ONE platform, the same numbers as the Analytics page of dfl-campaigns. daily: for each day, the sum over this account’s posts of each post’s latest cumulative views on or before that day (a post counts from its first snapshot in the window). top: the 5 posts with the most latest views. median_views: median latest views of posts dispatched with scheduled_for inside the window. Never add two accounts or platforms together; call once per account. truncated=true means the page cap was hit and totals may be low. |
list_social_accounts | List campaigns.social_accounts: each Zernio account with its business unit (name, logo) and its owner (name, avatar). All filters combine with AND. unmapped_only returns the accounts with no business_unit_id. Write a mapping with set_social_account_mapping. |
set_social_account_mapping | Create or update the campaigns.social_accounts row of one Zernio account (idempotent UPSERT on zernio_account_id). It maps the account to a business unit (strategy.business_units) and to the person who owns it (public.profiles). An omitted optional field keeps its stored value; an explicit null clears business_unit_id or owner_user_id. Take zernio_account_id, zernio_profile_id and platform from list_zernio_accounts. The BU must exist: find it with list_business_units on the strategy MCP, or create a creator BU there with create_business_unit — this server does not create BUs. Runs with your JWT; RLS allows the write only to global admins. |
list_post_business_units
Section titled “list_post_business_units”List post calendar business units
The business units the post calendar can schedule for — strategy.business_units, archived ones excluded. Call this FIRST: every other tool takes a business_unit_id (a uuid), never a name like “itera”.
Takes no parameters.
list_zernio_profiles
Section titled “list_zernio_profiles”List Zernio profiles and their accounts
Who publishes where: each Zernio profile (a person or the company) with its connected accounts per network, plus the accounts that belong to no profile. Call this FIRST when drafting a post: draft_post_for_review requires zernio_profile_id — the profile of the person or brand the post goes out as (“Criador” in the dfl-campaigns UI). A post is NOT limited to that profile’s own accounts — the app allows channels across profiles (e.g. the brand DevFellowship plus a team member’s personal accounts); the profile just names who the post is mainly for.
Takes no parameters.
list_zernio_accounts
Section titled “list_zernio_accounts”List connected Zernio accounts
The social accounts connected in Zernio, each with the id a channel needs to actually publish. Call this BEFORE draft_post_for_review whenever the post has channels: Zernio publishes per ACCOUNT, and there is more than one account on the same platform (a company profile and a personal one), so “instagram” alone does not say where the post goes out. A channel drafted without its zernio_account_id is dropped at dispatch, and the review queue cannot add the account afterwards — that post has to be redone.
Takes no parameters.
list_posts
Section titled “list_posts”List calendar posts
Read the post calendar, ordered by scheduled_for. Without business_unit_id it is the consolidated view across every BU. Pass status: “awaiting_review” to read the human review queue — everything the AI wrote that is still waiting on a person. assignee_id and zernio_profile_id narrow it to one person or one profile; while those columns do not exist yet the filter is skipped and notes says so. Each post carries assignment; pass include_metrics: true to attach latest_metrics per platform and account. Each post carries archetype and media_group (its content-format tag; null when unset).
| Parameter | Type | Required | Description |
|---|---|---|---|
business_unit_id | string | no | Filter to one BU (from list_post_business_units) |
status | enum | no | Filter by lifecycle state One of: draft, awaiting_review, approved, rejected, dispatched. |
scheduled_from | string | no | Only posts scheduled at/after this ISO date-time |
scheduled_to | string | no | Only posts scheduled at/before this ISO date-time |
assignee_id | string | no | Only posts assigned to this user id |
zernio_profile_id | string | no | Only posts linked to this Zernio profile |
include_metrics | boolean | no | Attach latest_metrics per platform and account to each post (never summed across platforms) Default: false. |
limit | number | no | Default: 50. |
list_core_daily_topics
Section titled “list_core_daily_topics”List Core Daily topics per person
What each person said in the last N days, merged per person from the Discord channel #core-daily-updates (primary) and the Core Daily meeting transcripts (when there was one). Each line has source (“channel” with a Discord url, or “meeting”); lines under 8 words and repeats are removed, at most 30 per person, newest first. sources says which side answered. Read-only. Use it to propose one draft per person: each line is raw speech, so rewrite it into a hook before calling draft_post_for_review, and pass assignee_id = user_id when it is present (user_id is only set on an exact name match with a member).
| Parameter | Type | Required | Description |
|---|---|---|---|
days | number | no | Window in days, 1 to 30 Default: 7. |
speaker | string | no | Only this speaker (case and accents ignored), as shown in speaker |
list_suggested_topics
Section titled “list_suggested_topics”List suggested topics (Curadoria board)
Read the suggested topics on the Curadoria board of /posts/pautas in dfl-campaigns. A suggested topic (a curated content topic, “pauta” in Portuguese) is an idea for content that a person or an agent put on the board and that later turns into one or many posts. Each topic has a title, briefing, status (idea, todo, awaiting_content, in_production, awaiting_approval, posted, standby), optional due_date, formats, channels, reference_links, business_unit_id, assignee_id and a url to its card. Active topics are returned by default; pass archived=true for the archived ones. Filters are applied after the read. Read-only. Call it before create_suggested_topic so you do not add a duplicate. If the answer says the board is not live on this deployment, do not retry.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | enum | no | Only topics in this column One of: idea, todo, awaiting_content, in_production, awaiting_approval, posted, standby. |
assignee_id | string | no | Only topics assigned to this user |
business_unit_id | string | no | Only topics of this BU (from list_post_business_units) |
archived | boolean | no | true reads the archived topics instead of the active ones Default: false. |
create_suggested_topic
Section titled “create_suggested_topic”Add a suggested topic to the Curadoria board
Add one suggested topic to the Curadoria board of /posts/pautas in dfl-campaigns. A suggested topic (a curated content topic, “pauta” in Portuguese) is an idea for content that a person or an agent proposes and that later turns into one or many posts. Creating one posts NOTHING and drafts no post: it only puts a card on the board, marked as created by an agent (origin “mcp”), for a person to pick up. To draft a post use draft_post_for_review. The status defaults to idea. Run list_suggested_topics first to avoid duplicates. The answer carries the card url. A refusal (not available on this deployment, a rule, a missing BU or user) says do not retry; only a temporary failure may be retried.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Short name of the topic, as it shows on the card |
briefing | string | no | What the content is about and the angle (up to 5000 characters) |
status | enum | no | Board column; omitted means idea One of: idea, todo, awaiting_content, in_production, awaiting_approval, posted, standby. |
due_date | string | no | Day the content is wanted, YYYY-MM-DD |
formats | enum[] | no | Content formats |
channels | enum[] | no | Networks the content is for |
reference_links | string[] | no | http(s) links that inspired or back the topic |
business_unit_id | string | no | Which BU the topic is for (from list_post_business_units) |
assignee_id | string | no | The user expected to produce it |
update_suggested_topic
Section titled “update_suggested_topic”Edit a suggested topic on the Curadoria board
Edit the content of one suggested topic (“pauta”) on the Curadoria board of /posts/pautas in dfl-campaigns: title, briefing, due date, formats, channels, reference links, BU or assignee. Any member can edit any topic. Send only the fields to change; an omitted field keeps its value, and null clears briefing, due_date, business_unit_id or assignee_id. Arrays (formats, channels, reference_links) replace the whole list. It does NOT change the column or the order: use move_suggested_topic for that, and archive_suggested_topic to archive. It refuses an empty change, an unknown field and an unknown id. Get the id from list_suggested_topics. A refusal says do not retry; only a temporary failure may be retried.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The suggested topic id (from list_suggested_topics) |
title | string | no | New title |
briefing | string | no | New briefing; null clears it |
due_date | string | no | Day the content is wanted, YYYY-MM-DD; null clears it |
formats | enum[] | no | Replaces the formats |
channels | enum[] | no | Replaces the channels |
reference_links | string[] | no | Replaces the http(s) reference links |
business_unit_id | string | no | BU from list_post_business_units; null clears it |
assignee_id | string | no | The user expected to produce it; null clears it |
move_suggested_topic
Section titled “move_suggested_topic”Move a suggested topic to a column or position
Move one suggested topic (“pauta”) to a column of the Curadoria board of /posts/pautas in dfl-campaigns and, optionally, to a place inside it. status is the target column (it may be the current one, to only reorder). Without before_id/after_id the card goes to the end of the column; with before_id it lands right above that card, with after_id right below it (send one, never both). The neighbour must be an active topic already in the target column. Any member can move any topic. It does not edit content (update_suggested_topic) and does not touch posts. A refusal says do not retry; only a temporary failure may be retried.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The suggested topic to move (from list_suggested_topics) |
status | enum | yes | Target column One of: idea, todo, awaiting_content, in_production, awaiting_approval, posted, standby. |
before_id | string | no | Place the card right above this one |
after_id | string | no | Place the card right below this one |
archive_suggested_topic
Section titled “archive_suggested_topic”Archive or restore a suggested topic
Archive one suggested topic (“pauta”) of the Curadoria board of /posts/pautas in dfl-campaigns, or bring an archived one back with restore=true. Archiving hides the card from the board but keeps it, and it is reversible: this is the safe way to take a topic off the board. Any member can archive or restore any topic. To remove one for good use delete_suggested_topic. Archived topics are read with list_suggested_topics archived=true. A refusal (unknown id, not available on this deployment) says do not retry; only a temporary failure may be retried.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The suggested topic id (from list_suggested_topics) |
restore | boolean | no | true brings an archived topic back to the board; default archives Default: false. |
delete_suggested_topic
Section titled “delete_suggested_topic”Permanently delete a suggested topic you created
Permanently delete one suggested topic (“pauta”) from the Curadoria board of /posts/pautas in dfl-campaigns. It cannot be undone. Only the creator of the topic or an admin may delete it: the server decides that, from the caller session, and refuses everybody else with “do not retry” (ask the creator or an admin instead). To take a topic off the board without losing it, use archive_suggested_topic. Pass confirm_title equal to the topic’s current title, exactly as list_suggested_topics shows it; a different title, or an id that is not on the board, deletes nothing. Posts already drafted from the topic are not deleted.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | The suggested topic id (from list_suggested_topics) |
confirm_title | string | yes | The topic’s current title, copied exactly: the guard against deleting the wrong card |
get_post
Section titled “get_post”Get one calendar post
One post with its channels, its lifecycle state and its approval trail — who approved it and when, and whether it was already dispatched to Zernio. Also returns assignment (zernio_profile_id, assignee_id; null while those columns do not exist) and latest_metrics: the latest views/likes/comments per platform AND Zernio account, never summed across platforms. The post always carries archetype and media_group (its content-format tag; null when unset or while the columns do not exist). live is { published_at, published_url } once dfl-campaigns read the post LIVE in Zernio (status published): the link to the post on the network. null while it is not known live. Use it to answer “how did this post do” and “where is it”.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | — |
draft_post_for_review
Section titled “draft_post_for_review”Draft a post into the human review queue
Write one post into the calendar. It lands in awaiting_review and NOTHING leaves this server until a person opens the queue in dfl-campaigns, reads it and approves it — you cannot clear it yourself; approve_post only records a decision a person already gave you. Schedule the time you want it to go out; a channel only reaches its network if it carries the connected zernio_account_id. An Instagram Reel cover is optional: pass cover_media_id (the COVER_MEDIA_ID from the thumbify-reel-cover skill) or cover_url (a Thumbify render URL), or Instagram uses frame 0 of the video. A cover that is not a Thumbify cover is refused at approval. The Lesson Studio cover slide is NOT carried over. A carousel (Instagram, or a TikTok photo carousel) is either media_ids (existing public.media ids) or media_urls (rendered images, e.g. from render_composition_images; dfl-campaigns registers them as your media). A single image is media_id; TikTok accepts it as a photo post (JPEG, PNG or WebP, up to 20 MB each).
| Parameter | Type | Required | Description |
|---|---|---|---|
business_unit_id | string | yes | Which BU publishes it (from list_post_business_units) |
title | string | yes | Internal title — how the post shows up in the calendar |
body | string | yes | The text that goes out to the network |
scheduled_for | string | yes | ISO date-time the post should be published at |
youtube_visibility | enum | no | How the post lands on YouTube and TikTok. Omitted means private — the closed default, on purpose: a post that goes out more public than intended cannot be taken back, while a private one is one click away from being opened. On TikTok (video or photo) it sets the privacy level: private = only the creator, unlisted = followers only, public = everyone — so the default private means a TikTok post only the creator can see. Only matters when a channel is youtube or tiktok. One of: private, unlisted, public. |
link_url | string | no | INTERNAL note only — this URL is NOT published. The dispatch sends title, body and media to Zernio and nothing else, so a link (and any utm_ it carries) reaches the network ONLY if it is written inside body. The dfl-campaigns form stopped offering this field for that reason; it is kept here for rows written through the API. Put the link in body. |
media_id | string | no | public.media id — never a raw bucket URL; media is served by media.devfellowship.com/:id |
media_ids | string[] | no | Carousel (Instagram, or TikTok photo carousel): ordered public.media ids of 2 to 10 images; use instead of media_id. Never a single image: one image is media_id. |
media_urls | string[] | no | Carousel (Instagram, or TikTok photo carousel) from rendered images: 2 to 10 public devfellowship S3 media/ PNG/JPG URLs (e.g. render_composition_images output) in order; use instead of media_ids. Never together with media_id or media_ids. |
cover_media_id | string | no | Reel cover: another public.media id (JPEG or PNG, 1080x1920) holding a Thumbify cover (reel-cover-v<N>-…) — any other file is refused at approval. Not the video. Omitted means Instagram uses frame 0. Dispatch sends this as instagramThumbnail. |
cover_url | string | no | Reel cover as a URL, in place of cover_media_id: a public JPEG/PNG of the devfellowship S3 bucket under media/ — typically a Thumbify render output_url (media/renders/<template>/<ts>.png). dfl-campaigns registers it as public.media (owned by you) and stores that id. Other hosts are refused. Never both. |
instagram_collaborators | string[] | no | Instagram co-authors: up to 3 usernames, without the @. A co-authored post appears in the feed AND the grid of BOTH accounts, with both handles in the header and the reach added together — it is what makes a post published by a personal profile also work for the company account, without reposting. Naming the @ in the caption does NOT do this; that is only text. ⚠️ Tagging is not publishing: the tagged account gets an INVITE and has to accept it in the Instagram app, and until it does the post does not appear there. Neither Zernio nor Meta can accept for us. Dispatch sends these as collaborators, so they must be on the post BEFORE dispatch — they cannot be added to a post already sent. |
channels | object[] | no | Networks this post goes out through. The same content on several networks or accounts — even across profiles and business units (e.g. TikTok of a person plus YouTube Shorts of the brand) — is ONE call with several entries here, never one call per channel: each extra post is one more approval for the reviewer. Default: []. |
zernio_profile_id | string | yes | Zernio profile of the person or brand this post goes out as (“Criador” in the dfl-campaigns UI). Required on every post — get it from list_zernio_profiles. |
assignee_id | string | no | Who is expected to produce the content (“Solicitante” in the UI). null unassigns it. |
keyword_ids | string[] | no | SEO keywords (strategy.keywords ids, from list_keywords on the strategy MCP) this post was written for, same business unit as the post. The first one is the primary keyword: the post’s views count for it. Replaces the whole set; an empty array unlinks all. |
archetype | string | no | Content-format archetype of the post, as a lowercase slug (e.g. “talking-head-hook”), max 64 characters. Requires media_group. null clears it. |
media_group | enum | no | Media group of the post: vertical-short | long-video | micro-clips | ephemeral-daily | swipe-static. Required when archetype is set. null clears it (only when the post has no archetype). One of: vertical-short, long-video, micro-clips, ephemeral-daily, swipe-static. |
revise_post_in_review
Section titled “revise_post_in_review”Revise a post that has not been approved yet
Rewrite a post that is still in the queue — typically after a reviewer left review_notes, or when a rejected post is being redone. Only draft, awaiting_review and rejected posts can be revised: once a person has approved it, the text they read is the text that goes out. Status is never changed here.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | — |
title | string | no | — |
body | string | no | — |
scheduled_for | string | no | New ISO date-time |
link_url | string | no | INTERNAL note only, never published — the dispatch sends title, body and media to Zernio and nothing else. To change the link the network actually sees, edit body. null clears this note. |
media_id | string | no | null clears the media |
cover_media_id | string | no | Reel cover (JPEG/PNG public.media) holding a Thumbify cover — any other file is refused at approval. null clears it, and Instagram then uses frame 0. Not the video. |
cover_url | string | no | Reel cover as a URL, in place of cover_media_id: a public JPEG/PNG of the devfellowship S3 bucket under media/ — typically a Thumbify render output_url (media/renders/<template>/<ts>.png). dfl-campaigns registers it as public.media (owned by you) and stores that id. Other hosts are refused. Never both. Replaces the cover. |
youtube_visibility | enum | no | New visibility — private, unlisted or public. Also sets TikTok privacy: private = only the creator, unlisted = followers only, public = everyone. One of: private, unlisted, public. |
instagram_collaborators | string[] | no | Replaces the co-author list. An empty array clears it. Instagram co-authors: up to 3 usernames, without the @. A co-authored post appears in the feed AND the grid of BOTH accounts, with both handles in the header and the reach added together — it is what makes a post published by a personal profile also work for the company account, without reposting. Naming the @ in the caption does NOT do this; that is only text. ⚠️ Tagging is not publishing: the tagged account gets an INVITE and has to accept it in the Instagram app, and until it does the post does not appear there. Neither Zernio nor Meta can accept for us. Dispatch sends these as collaborators, so they must be on the post BEFORE dispatch — they cannot be added to a post already sent. |
zernio_profile_id | string | no | Zernio profile of the person or brand this post goes out as (“Criador” in the dfl-campaigns UI), from list_zernio_profiles. null unlinks it. |
assignee_id | string | no | Who is expected to produce the content (“Solicitante” in the UI). null unassigns it. |
archetype | string | no | Content-format archetype of the post, as a lowercase slug (e.g. “talking-head-hook”), max 64 characters. Requires media_group. null clears it. |
media_group | enum | no | Media group of the post: vertical-short | long-video | micro-clips | ephemeral-daily | swipe-static. Required when archetype is set. null clears it (only when the post has no archetype). One of: vertical-short, long-video, micro-clips, ephemeral-daily, swipe-static. |
approve_post
Section titled “approve_post”Record a human approval taken outside the review queue
Record that a PERSON cleared this post for publishing, when they said so to you instead of clicking in the dfl-campaigns UI — the case the queue had no answer for (Tainan asked for a Reel over Telegram, 2026-09-11). It does not approve on your behalf: it WRITES DOWN a decision a human already took, and approval_reference is where you point at the message they took it in. Never call this off your own judgement and never invent a reference to satisfy the field — no server can tell a made-up pointer from a real one, which is precisely why the queue exists. If nobody told you to publish, leave the post in awaiting_review and say it is waiting on a person. Restricted to super admins; everybody else is refused and has to approve in the UI. Approving IS publishing: the same call sends the post to Zernio, scheduled for its scheduled_for (a time that already passed goes out now), and Zernio publishes it then. If Zernio refuses, this call fails and the post stays approved but NOT scheduled — fix what the error names and retry with dispatch_post. If the error says it cannot tell whether Zernio received the post, do NOT retry: tell the person to check Zernio.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | The post the person cleared (from list_posts with status: “awaiting_review”) |
approval_reference | string | yes | Where the human said “publish it”, in their words or as a locator: “Telegram msg 18508”. The only trace of the person on an approval with no click — it must point at something real that someone auditing this post later can go read. |
expected_updated_at | string | yes | The post’s updated_at exactly as you read it (get_post / list_posts) when you showed it to the person. If the post was revised since, the server refuses with 409: read it again and get the person’s go-ahead on the new content. |
review_notes | string | no | What they said along with it, if anything — kept on the post as the reviewer note |
dispatch_post
Section titled “dispatch_post”Retry scheduling an approved post in Zernio
Approving a post already sends it to Zernio — you do not need this after approve_post or after a person approves in the UI. Use it only to RETRY a post that is approved but not scheduled, because Zernio refused it at approval time (the approval error said so). Fix what that error named first, or the retry fails the same way. The server refuses anything that is not approved with a recorded approver, so this can never publish something nobody cleared. A post whose send outcome is unknown stays dispatched without a Zernio id and is refused here on purpose, so it is never published twice — tell the person to check Zernio. Zernio publishes at scheduled_for; a time that already passed goes out now, and the calendar is updated to match.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | The approved, not yet scheduled post (list_posts with status: “approved”) |
unschedule_post
Section titled “unschedule_post”Take a scheduled post back out of Zernio
Stop a post that Zernio is holding from publishing: the server cancels it in Zernio FIRST and only then rewrites the row, which comes back as awaiting_review with the approval trail cleared. It is NOT a rejection and NOT a shortcut around the queue — it only moves a post BACKWARDS, into human review, and someone has to approve it again for it to be scheduled at all. Judging the post is still not something this server does. A new scheduled_for is required because it is what the post holds while it waits (and it has to be in the future). This only works while Zernio still HOLDS the post: once it published — which includes the last few minutes before scheduled_for, when Zernio may already be sending — nothing here takes it back, and the call is refused saying so rather than reporting a cancel that did not happen. Deleting what is already on the network is done in the network itself, by a person.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | The scheduled post (from list_posts with status: “dispatched”) |
scheduled_for | string | yes | New ISO date-time the post waits for in the queue. Must be in the future — the server refuses a date that already passed. |
review_notes | string | no | Why it was pulled, kept on the post as the reviewer note for whoever reads it next |
reject_post
Section titled “reject_post”Withdraw one of your own posts from the review queue
Move a post that YOU created from awaiting_review to rejected — for example a smoke or test post, or a post the person told you to drop. It refuses any post created by somebody else: rejecting another person’s post is a judgement on content, and a person does that in the dfl-campaigns UI. It also refuses a draft (delete it with delete_post), an approved post, and a scheduled or published post (unschedule_post takes a scheduled post back to the queue). The reason is stored as the post’s review_notes. A rejected post never publishes; delete_post then removes it.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | Your post, from list_posts with status: “awaiting_review” |
reason | string | yes | Why the post leaves the queue, kept as review_notes: “smoke test post, not for publishing” |
delete_post
Section titled “delete_post”Delete one of your own draft or rejected posts
Permanently delete a post that YOU created, while it is a draft or rejected — for example a smoke or test post. The post and its channels go; this cannot be undone. It refuses a post created by somebody else, a post in the review queue (reject it with reject_post first), and every approved, scheduled or published post.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | Your draft or rejected post |
archive_post
Section titled “archive_post”Archive a published post (admin only) — hide it, keep its metrics
Hide an approved or PUBLISHED post from dfl-campaigns — the calendar, the profile pages (recent posts, top posts, cadence, counts) and analytics — while KEEPING its metric snapshots in the database. Use it for a published post that should not be in the lists, for example a test post that already went live. It is a soft delete that records who archived the post and why; it does not delete anything and it does not touch Zernio or the social network (the post stays live there, or has already expired). There is no un-archive tool. Restricted to global admins (IAM level 80 or higher): everybody else gets 403. It refuses a draft or rejected post (use delete_post), a post in the review queue (reject_post, then delete_post), and a post still scheduled in Zernio (unschedule_post first). Only call it when a person asked for the post to go, and put where they asked in the reason.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | The approved or published post to hide |
reason | string | yes | Why the post leaves the lists, pointing at the decision: “Test Stories, Tainan TG msg 19998/20007”. Stored on the post for whoever audits it later. |
set_post_keywords
Section titled “set_post_keywords”Link a post to the SEO keywords it was written for
Replace the SEO keywords linked to a post, in any status — including posts already dispatched, so older content can be mapped to keywords. It never changes the post itself and never touches its approval. The first keyword is the primary one.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | — |
keyword_ids | string[] | yes | — |
set_post_content_tag
Section titled “set_post_content_tag”Set the content-format tag of a post (any status)
Write media_group + archetype on ONE post, in any status — published posts included. It writes those two fields and nothing else: title, text, media, schedule and approval stay as they are. The creator of a post may tag it; any other post needs a global admin (IAM level >= 80). The pair must match the content-visual taxonomy (e.g. vertical-short + talking-head; archetype null is a media-group-only tag). Pass dry_run: true to see the before/after and the authority check without writing.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | — |
media_group | enum | yes | Media group: vertical-short | long-video | micro-clips | ephemeral-daily | swipe-static One of: vertical-short, long-video, micro-clips, ephemeral-daily, swipe-static. |
archetype | string | yes | Archetype slug of that media group: animated-explainer, talking-head, face-cam-over-screen, myth-vs-fact, quiz-spot-the-bug, long-tutorial, stories-frames, framework-carousel. null = media group only. |
dry_run | boolean | no | true = check and show the change, write nothing |
draft_story_sequence_for_review
Section titled “draft_story_sequence_for_review”Draft an Instagram Story sequence into the human review queue
Write an ordered Instagram Story sequence: one post per video part, published in index order a few minutes apart. Get media_ids from the studio tool split_export_for_stories, and pass them IN THAT ORDER — the order of the list is the order on Instagram. A single Story (one video of 3 to 60 s) is a sequence of 1: pass one media id. Instagram only, exactly one account. Every part lands in awaiting_review and NOTHING is published until a person approves the sequence (in the dfl-campaigns UI, or through approve_story_sequence with the reference of the message where the person said so). Optional user_tags: [{username, x?, y?}], up to 3 (our limit). It tags other Instagram accounts on the Stories. One list for the whole sequence: it is stored on every part and sent to Zernio as platformSpecificData.userTags, on Stories only. A leading ”@” is stripped and names are lowercased; x and y (0.0 to 1.0) come together or not at all. Zernio (docs.zernio.com/platforms/instagram): “Images require x/y (0.0 to 1.0); Reels and videos ignore coordinates; Stories take them optionally.” The tagged account must be a public Business or Creator account. WHAT INSTAGRAM RENDERS ON A STORY WAS NOT VERIFIED: it may be a plain tag and not the interactive mention sticker. Do not promise the person a clickable mention. The tags are fixed at draft time and are shown on the review page and in get_story_sequence (sequence.user_tags, parts[].user_tags). Until the dfl-schema migration that adds campaigns.posts.story_user_tags is applied, a call with user_tags answers 503 story_sequence_schema_missing and creates nothing; a call without user_tags works as before. Returns the sequence and review_url: send that link to the person who reviews it.
| Parameter | Type | Required | Description |
|---|---|---|---|
business_unit_id | string | yes | Which BU publishes it (from list_post_business_units) |
title | string | yes | Internal title. Each part shows up in the calendar as “<title> (i/N)”; a single Story keeps “<title>”. |
body | string | yes | The text that goes with the Stories |
zernio_account_id | string | yes | The connected Instagram Zernio account that publishes (from list_zernio_accounts) |
media_ids | string[] | yes | public.media ids of the Story videos, 1 to 25, in order — as split_export_for_stories returns them. One id = a single Story. |
first_at | string | no | ISO date-time of part 0. Omitted means now + 5 minutes. The next parts follow at gap_minutes intervals. |
gap_minutes | number | no | Minutes between two parts, 1 to 5. Omitted means the server default (2). |
user_tags | object[] | no | Instagram accounts to tag on every Story of the sequence, up to 3: [{username, x?, y?}]. x and y are 0.0 to 1.0, both or neither. Public Business or Creator accounts only. What Instagram shows on a Story was not verified. |
get_story_sequence
Section titled “get_story_sequence”Get one Instagram Story sequence
One Story sequence with every part in index order: status, scheduled_for, updated_at, the approval trail and the Zernio id, and the user tags (sequence.user_tags and parts[].user_tags; absent when the dfl-schema column story_user_tags is not applied yet). Read it before approve_story_sequence, and show the person what they approve.
| Parameter | Type | Required | Description |
|---|---|---|---|
group | string | yes | The sequence group id (the group that draft_story_sequence_for_review returned) |
approve_story_sequence
Section titled “approve_story_sequence”Record a human approval of a Story sequence taken outside the review queue
Record that a PERSON cleared this whole Story sequence for publishing (show them its user_tags first: the tags go to Instagram with the Stories, and what Instagram renders was not verified), when they said so to you instead of clicking in the dfl-campaigns UI. It does not approve on your behalf: it WRITES DOWN a decision a human already took, and approval_reference is where you point at the message they took it in. Call it ONLY with an explicit human approval and the reference of that message. Never call this off your own judgement and never invent a reference to satisfy the field — no server can tell a made-up pointer from a real one. If nobody told you to publish, leave the sequence in awaiting_review and say it is waiting on a person. Restricted to super admins; everybody else is refused and has to approve in the UI. Approving IS publishing: the same call sends the parts to Zernio as Stories, in index order. If one part fails, the later parts are not sent (the order is kept); the answer says which part and what to do. If it says nobody can tell whether Zernio received a part, do NOT retry: tell the person to check Zernio.
| Parameter | Type | Required | Description |
|---|---|---|---|
group | string | yes | The sequence group id (the group that draft_story_sequence_for_review returned) |
approval_reference | string | yes | Where the human said “publish it”, in their words or as a locator: “Telegram msg 18508”. The only trace of the person on an approval with no click — it must point at something real that someone auditing this sequence later can go read. |
expected_updated_at | object | no | Map of every part id to its updated_at, exactly as you read it (get_story_sequence) when you showed the sequence to the person. If a part was revised since, the server refuses with 409: read it again and get the go-ahead on the new content. Omitted means the tool reads the sequence now and uses its current values. |
review_notes | string | no | What they said along with it, if anything — kept as the reviewer note |
first_at | string | no | ISO date-time of part 1. A future time is honored. A time earlier than now + 30 s moves to now + 30 s. The answer returns first_at: the time asked for, the time sent, and whether it moved. Omitted means the stored time of part 1. |
gap_minutes | number | no | Minutes between two parts, 1 to 5. Omitted means the server default (2). |
check_story_sequence_order
Section titled “check_story_sequence_order”Check that the Stories of a sequence went out in order
Read from Zernio when each part of the sequence was published, and compare the order. check.ok is true only when every checked part went out after the part before it. check.violations names the parts out of order; check.incomplete names the parts that are not published yet (check again later). parts[].outcome: ok = live (publishedUrl is the live link), pending = Zernio holds it, not_sent = never sent (post_status says approved or awaiting_review; it is not a refusal), rejected = Zernio refused it or reports it failed, unknown = check Zernio. It sends nothing. It records each part that Zernio reports live as published, with its live URL.
| Parameter | Type | Required | Description |
|---|---|---|---|
group | string | yes | The sequence group id (the group that draft_story_sequence_for_review returned) |
retry_story_sequence
Section titled “retry_story_sequence”Retry sending the rest of an approved Story sequence
Use it only after approve_story_sequence (or a later retry) stopped at a part that Zernio rejected or refused. It sends the parts that are approved but not scheduled, in index order. It never approves anything: the server refuses a part that no person cleared. action “none” means there was nothing to send. A 409 retry_stopped means the server will not retry — for example a part whose send outcome is unknown. Then tell the person to check Zernio.
| Parameter | Type | Required | Description |
|---|---|---|---|
group | string | yes | The sequence group id (the group that draft_story_sequence_for_review returned) |
gap_minutes | number | no | Minutes between two parts, 1 to 5. Omitted means the server default (2). |
get_business_unit_voice
Section titled “get_business_unit_voice”Get business unit voice
READ the brand VOICE of a business unit before you write any post copy — the writing patterns that say how that BU sounds. Source is strategy.writing_patterns, the single canonical store, read through the strategy MCP tool list_writing_patterns with your own JWT. Call it after list_post_business_units and BEFORE draft_post_for_review: a Reel written without it is written in a voice you guessed. Each row is one “slot” (e.g. “book”, “youtube”); the free-form pattern jsonb carries description, toneAxes, vocabularyDo / vocabularyAvoid and examplePairs. Optionally narrow to one slot. This tool is READ-ONLY: authoring a voice happens in BM Canvas or on the strategy MCP.
| Parameter | Type | Required | Description |
|---|---|---|---|
business_unit_id | string | yes | The BU whose voice to read (from list_post_business_units) |
slot | string | no | Narrow to a single voice slot, e.g. “book”, “youtube”, “pedagogia_aula” |
list_campaign_accounts
Section titled “list_campaign_accounts”List campaign analytics accounts
List each platform and Zernio account combination that has stored post metric snapshots. Accounts stay separate even when they use the same platform. An RLS-safe database RPC excludes soft-deleted posts before it computes the summaries.
| Parameter | Type | Required | Description |
|---|---|---|---|
platform | enum | no | Filter to one social platform One of: instagram, facebook, linkedin, tiktok, youtube, x, threads, pinterest, reddit, bluesky, telegram, discord, whatsapp, google_business. |
rank_account_posts
Section titled “rank_account_posts”Rank posts for one account
Rank one Zernio account’s campaigns posts. lifetime uses the latest absolute counter. period_gain subtracts the baseline from the latest observation inside the requested period and can be negative. The RLS-safe database RPC computes and bounds the ranking.
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | Exact Zernio account ID from list_campaign_accounts |
platform | enum | yes | Exact platform from list_campaign_accounts One of: instagram, facebook, linkedin, tiktok, youtube, x, threads, pinterest, reddit, bluesky, telegram, discord, whatsapp, google_business. |
metric | enum | yes | One of: views, likes, comments. |
basis | enum | no | One of: lifetime, period_gain. Default: "lifetime". |
start_date | string | no | Period start date, required for period_gain |
end_date | string | no | Period end date, required for period_gain |
limit | number | no | Default: 10. |
get_post_metric_history
Section titled “get_post_metric_history”Get post metric history
Return stored daily absolute counters for one campaigns post. Account and platform targets are required so separate publication targets are never combined. Reads fail explicitly above 10,000 visible snapshots; use a smaller date range.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | campaigns.posts ID |
account_id | string | yes | Exact Zernio account ID |
platform | enum | yes | Exact publication platform One of: instagram, facebook, linkedin, tiktok, youtube, x, threads, pinterest, reddit, bluesky, telegram, discord, whatsapp, google_business. |
start_date | string | no | — |
end_date | string | no | — |
get_account_analytics
Section titled “get_account_analytics”Get 30-day analytics for one account
The last 30 São Paulo days of ONE Zernio account on ONE platform, the same numbers as the Analytics page of dfl-campaigns. daily: for each day, the sum over this account’s posts of each post’s latest cumulative views on or before that day (a post counts from its first snapshot in the window). top: the 5 posts with the most latest views. median_views: median latest views of posts dispatched with scheduled_for inside the window. Never add two accounts or platforms together; call once per account. truncated=true means the page cap was hit and totals may be low.
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | Exact Zernio account ID from list_campaign_accounts |
platform | enum | yes | Exact platform from list_campaign_accounts One of: instagram, facebook, linkedin, tiktok, youtube, x, threads, pinterest, reddit, bluesky, telegram, discord, whatsapp, google_business. |
list_social_accounts
Section titled “list_social_accounts”List Zernio account mappings
List campaigns.social_accounts: each Zernio account with its business unit (name, logo) and its owner (name, avatar). All filters combine with AND. unmapped_only returns the accounts with no business_unit_id. Write a mapping with set_social_account_mapping.
| Parameter | Type | Required | Description |
|---|---|---|---|
business_unit_id | string | no | Only accounts of this BU |
owner_user_id | string | no | Only accounts owned by this user |
zernio_profile_id | string | no | Only accounts of this Zernio profile |
unmapped_only | boolean | no | Only accounts with no business_unit_id |
set_social_account_mapping
Section titled “set_social_account_mapping”Set the BU and owner of a Zernio account
Create or update the campaigns.social_accounts row of one Zernio account (idempotent UPSERT on zernio_account_id). It maps the account to a business unit (strategy.business_units) and to the person who owns it (public.profiles). An omitted optional field keeps its stored value; an explicit null clears business_unit_id or owner_user_id. Take zernio_account_id, zernio_profile_id and platform from list_zernio_accounts. The BU must exist: find it with list_business_units on the strategy MCP, or create a creator BU there with create_business_unit — this server does not create BUs. Runs with your JWT; RLS allows the write only to global admins.
| Parameter | Type | Required | Description |
|---|---|---|---|
zernio_account_id | string | yes | Zernio account id (the conflict key) |
zernio_profile_id | string | yes | Zernio profile id of the account |
platform | string | yes | Zernio platform string, e.g. instagram, youtube. Stored lowercase. |
handle | string | no | Zernio username. Omit to keep the stored value; null clears it. |
display_name | string | no | Zernio displayName. Omit to keep the stored value; null clears it. |
avatar_url | string | no | Zernio profilePicture URL. Omit to keep the stored value; null clears it. |
business_unit_id | string | no | strategy.business_units id. Omit to keep; null clears (account not clickable). |
owner_user_id | string | no | User id of the person who owns the account (public.profiles id). Omit to keep; null clears. |