Save Post
save_postCreates a new draft (omit postId; channel required) or edits an existing post: text, channel fields, media, angle, pillar, buyer questions, proposed time. Never submits or publishes. Returns a save receipt: scheduledAt, assetIds, channel, counted body length, buyerQuestions, idea, language and translationOf as stored, updatedAt, and checks (Mark's content checks on the saved content, including placeholder, and keyword on a website post with meta.seo.keyword; a fail is not a refused save, but submit_post refuses a draft while it fails). On a draft or changes-requested post, rev does not change; use updatedAt to confirm the save. A post in review, approved or scheduled needs rev (from get_post): the edit saves as the next rev and the post goes back to in review, so a person approves it again; a stale rev is refused, and so is a phrase this workspace blocks, a placeholder like [needs source], a new website slug another page in your CMS uses, or no language on a website post whose CMS collection stores pages per language. On those posts ideaId or guideVersion alone is refused, since it would drop the approval without changing what publishes. To link any post to an idea without touching its approval, call save_idea with linkPostIds. Posts in any other status are locked; to fix the text of a published social post, call edit_published_post. Any post that is not archived, published and imported posts included, takes postId with angleId and/or pillarId alone (rev optional): that changes only its pillar, angle and persona, so its status, rev, content and approval stay as they are. A website post that is not archived (in review, approved, scheduled, published, failed or missed) also takes postId with buyerQuestionIds alone: that replaces the buyer questions it answers and nothing else, so its status, rev, content and approval stay as they are. Asset ids that are not in this workspace are listed as dropped, never attached silently. Likewise droppedMetaKeys lists the meta keys the channel does not take, at any depth, as paths like cover.url or cmsImages[0].alt, which the save ignored; the save still succeeds. A LinkedIn post can carry one PDF asset (kind document) and no other media, up to 100 MB; meta.documentTitle is the title LinkedIn shows above it, else the file name. A PDF with other media, over 100 MB, or on another channel is refused with the message submit_post gives. An angle sets the post's pillar and persona to the angle's; a pillarId that contradicts the angle is refused. Website posts (blog, faq, competitor_page) take a pillar, never an angle, plus the buyer questions the page answers (buyerQuestionIds, from get_ai_visibility). A LinkedIn Article (linkedin_article) takes a Markdown body and meta { title, cover? }; it needs no connected account, and once approved it waits for a person to publish it on LinkedIn and record the URL with record_post_published. ideaId links the post to an idea from list_ideas (null unlinks); linking never changes the post's pillar, persona or angle. A new draft with ideaId takes what the call leaves unset from the idea: its pillar and angle (a website post takes the pillar and persona, never the angle), its buyer question on a website post while that question is active, and a body that starts with the idea's title and brief. Call get_channel_rules first. To send it for review, call submit_post after saving. Related: get_post, submit_post, add_asset, list_ideas.
Tool call template
This tool declares no required parameters. Add optional parameters from the input schema as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {},
"name": "save_post"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rev | No | The post's current rev from get_post. Required to edit a post in review, approved or scheduled; ignored on drafts. | |
| body | No | Post body / caption. | |
| meta | No | Channel-specific fields from get_channel_rules. Instagram, LinkedIn, Facebook and X: altText is the post-wide alt text and altTexts is one alt text per image in media order (on X, post 1's images, then each reply's, in thread order; when threadMedia moves media, send altTexts in the new order); a blank entry uses altText, and videos and PDFs take none. LinkedIn: documentTitle is the line LinkedIn shows above an attached PDF; blank uses the file name. Social posts (not YouTube): cmsImages: [{ mediaId, collection }] attaches images already in the connected CMS (search_cms_media), after the assets on post 1; collection is the media collection search_cms_media named (left out, the default from get_website_cms; one Mark cannot read is refused). Mark fills in the type and size of each, takes JPEG, PNG, WebP or GIF, and copies them to the platform at scheduling. They count toward media limits like assets. mediaOrder sets the order of the media on post 1 across assets and CMS images: a list of keys, an asset id as is or cms:~<collection>:<mediaId> for a CMS image (get_post shows the collection of each; an image saved before names a channel instead, cms:<channel>:<mediaId>; cms::<mediaId> for an image sent without either). An unknown or repeated key is dropped and media it leaves out goes last; without it, assets go first, then CMS images. altTexts follow this media order. meta without mediaOrder keeps the saved order; [] resets it. On Instagram, LinkedIn, Facebook, X and Pinterest, a newly attached CMS image with blank alt text takes the CMS alt (its altTexts entry on a post with several media, else altText; on Pinterest only when it is the only image on the pin). Edit or clear it like any alt text. X: thread is the replies after the body, in order; threadMedia puts media on a reply: one list of asset ids per thread entry, in the same order ([[], ["id"]] puts id on post 3). Every threadMedia id must also be in assetIds and on one post only; post 1 gets the assetIds no reply lists. meta replaces the saved meta: when thread keeps its number of replies and threadMedia is left out, the saved threadMedia carries over; when the number changes, send threadMedia too. Each X thread post takes up to 4 images, or 1 video, or 1 GIF. Website posts (blog, faq, competitor_page): the cover is cover: { source: "asset", assetId, alt } with an image id from search_assets / add_asset, or cover: { source: "cms", mediaId, collection, alt? } with an image already in the connected CMS from search_cms_media called with the same channel (collection left out: the default for that channel; a cover comes only from the collection the cover field accepts, else where new media goes, and any other is refused; Mark fills its type and size, alt defaults to the CMS alt, and an edited alt is written back to that CMS image on approve). alt is required before submit when the CMS takes a cover. Omit cover for no cover. An image already in the CMS goes in the body as  on its own line, with url from search_cms_media (any readable collection): on a rich-text mapping it publishes by reference with no upload and shows the alt text the CMS has (get_channel_rules says which). | |
| ideaId | No | Idea id from list_ideas, or null to unlink. Archived ideas can't be newly linked. On a new draft, unset pillar, angle, buyer questions and body come from the idea. | |
| postId | No | Existing post to edit. Omit to create a new draft. | |
| angleId | No | Angle id, or null to clear. Social posts only; refused on website posts. Sets the post's pillar and persona to the angle's. | |
| channel | No | Required to create a draft. When editing, omit it or pass the post's current channel — the channel cannot be changed here. | |
| assetIds | No | Asset ids from search_assets / add_asset, in order. Instagram takes at most 10; more is refused with an assetIds error. An X thread lists every asset in the thread here; meta.threadMedia says which reply each goes on. Website posts take no attachments: set meta.cover instead (assetIds may be omitted or hold only the cover id); other images go in the body as Markdown. A library image goes in a website post body as , with the relative url from search_assets / add_asset (/api/assets/<id>/content); save stores that form whatever host you write. | |
| language | No | Website posts only: the language the post is written in, a BCP 47 code such as de or de-CH, stored canonical (de-ch saves as de-CH). A CMS that stores languages (Payload localization) gets it as the locale: the page is saved in that language, not the site's default. null or blank clears it; omit to keep it. Refused on social posts. | |
| pillarId | No | Pillar id, or null to clear. With an angle, omit it or pass the angle's own pillar; any other value, null included, is refused. | |
| scheduledAt | No | Proposed publish time as a UTC ISO string, or null. | |
| guideVersion | No | guideVersion from get_drafting_guide for this channel, after checking the draft against that guide. | |
| translationOfId | No | Website posts only: the id of the post this one translates (the original). The original must be a website post in this workspace with a language, not itself a translation, and in another language than this post, which needs a language too; each language takes one translation. On Payload, Mark writes this post into the original's page in this post's language instead of making a new page, so publish the original first. Publishing either one makes every language of that page live. null clears it; omit to keep it. | |
| buyerQuestionIds | No | Website posts only: the buyer question ids this page answers, from get_ai_visibility. Replaces the set; null or [] clears it; omit to keep it. Active questions only, except ones already linked. Refused on social posts. Sent alone with postId, it also changes a submitted or live website post, without review; refused once archived. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rev | Yes | ||
| idea | Yes | The idea the post is linked to after this save. | |
| appUrl | Yes | ||
| assets | Yes | ||
| checks | Yes | Mark's content checks on the saved content: exclusion, char_limit, required_meta, ai_fluff, unsourced_number, placeholder, on a competitor_page also table and open_placeholder, and on a website post with meta.seo.keyword also keyword. On a draft, a fail stops submit_post; warn never does. | |
| guided | Yes | Whether the post was saved against the workspace's current drafting guide. | |
| postId | Yes | ||
| status | Yes | ||
| channel | Yes | ||
| assetIds | Yes | ||
| language | Yes | The post's language as stored (canonical, e.g. de-CH), or null. | |
| updatedAt | Yes | ||
| bodyLength | Yes | ||
| contentHash | Yes | ||
| scheduledAt | Yes | ||
| guideVersion | Yes | The drafting guide version recorded on the post, or null. | |
| translationOf | Yes | The post this one translates after this save, or null. | |
| buyerQuestions | Yes | The buyer questions the post answers after this save. | |
| characterLimit | Yes | ||
| droppedAssetIds | Yes | ||
| droppedMetaKeys | Yes | Meta keys this channel does not take, at any depth, as paths like cover.url or cmsImages[0].alt, so the save ignored them. Empty when none. |