Put an approved Senso draft live where AI models can cite it, and follow what happens to it. Use for publishing generated content ("publish this", "make it live", "push it to citeables"), checking or retrying where it went ("did it publish", "retry the failed one"), pulling it back ("unpublish", "take it down"), and for what happened after — citations, which prompts a page is winning, and the provenance audit of a live URL. Also destinations and the call-to-action card. The publish step of senso-verification-loop. Not for writing or checking content — that is senso-generate-verify.
npx @senso-ai/shipables install senso-ai/senso-publishTakes a draft that has been approved and evaluated, and puts it on a public page.
This is the first outward-facing action in the whole flow. The knowledge base is private, the
gap report is private, a draft is private. engine publish puts a page on a public shared
domain where anyone — and every AI model — can read it.
Exit condition: a live page at a known URL, with every publish record confirmed.
"Publish it" is the consent. The destination is a choice once, then a disclosure. Approving a draft is not consent to publish it, so publishing is never rolled into approval. On the organization's first publish — nothing live on any destination — the ask presents where the page goes, what that domain is, and the other domains it could go to, in the same breath as the publish ask; the user's answer is the consent and the destination together. After that, once the user has said publish — in those words, or by taking a "publish" option they were offered — do not ask again. What remains is one line before the command: where it goes, that the page is public, whether a card attaches, that it can be pulled back. Then run it. Ask a question only when there is a real choice to make: more than one destination would be hit, a custom domain has appeared since the last publish, or a card would attach that they have not seen.
The reads are yours. The user gets the conclusion. destinations list, the selection flag,
ctas for-content, ctas list — these are checks you run, not things you narrate. Someone who
said "publish it" does not want eighteen rows, what selected_for_generation means, how default
resolves, or how a cache works. The other domains are offered in a clause — a few names and a
count — with the full list one ask away. One line before, the result after. Explain a mechanism
only when they ask, or when it changed the outcome.
Read the publish records, not the success line. "Content published." reports that the
request went through, not that every destination succeeded. One publish makes one record per
destination, and one can fail while the others go live. The records are in the response —
read them.
Live-page configuration is fenced. Several commands here rewrite pages that are already
public. They run only on an explicit request, never as tidying. See Fenced off in
references/after-publishing.md.
A choice is offered as options, not as a sentence. Where the harness has a structured-choice
tool (Claude Code's AskUserQuestion), every two-to-four-answer decision in this file — yes/no
included — goes through it: the quoted ask is the question text, each door is an option with a
one-line description, the recommended one goes first. Free-text answers — a path, a figure, a
verdict — stay as prose. With no such tool (Codex, Cursor), the quote is the ask, verbatim. Each
ask below carries an Options: line naming its set.
senso content verification --status draft --output table --quiet # what is waiting
senso destinations list --output table --quiet
| State | Go to |
|---|---|
| An approved draft, not yet live | Before publishing, below |
| A publish that partly failed | references/after-publishing.md → Retries |
| Something is live and should not be | references/after-publishing.md → Pulling it back |
| "Is anyone citing it?" | references/after-publishing.md → What happened next |
| A live URL needs auditing | references/after-publishing.md → Provenance |
| The card, the destination list, a live page's settings | references/after-publishing.md → Fenced off — read it before touching any of them |
The invariant is senso-generate-verify's and it enforces it at the hand-off: the version that
publishes is the version that was judged — a completed run whose subject_version_id is the
draft's current version. Coming through the Verification Loop, that check has just run. Do not
repeat it.
Confirm it yourself only when you were not handed the draft — a bare "publish this" in a fresh session, or a returning-user option the loop offered off a looser read. Then one call settles it:
senso content versions <contentId> --output json --quiet # the current version, and when it was created
senso evals runs --subject-type content --from <version created_at> \
--limit 100 --output json --quiet
subject_version_id, not subject_id. Both are on the row. A completed run against an
earlier version of the same item is not a pass for this one — an edit after a run is precisely
what reopens it.evals runs is org-wide. --subject-type content filters by subject type, not by item, and
there is no --subject-id filter, so every content item's runs come back together. Bound it
with --from the version's created_at — a run cannot predate the version it judged, so that is
exactly the window. A bare --limit bounds the organization's whole run history instead, which
on an active organization returns a page with no run for this draft on it and reads as never
checked. Read total against the page and use --offset if it exceeds the limit.band: red, means stop and hand back. Do not publish and do not
evaluate here — checking belongs to senso-generate-verify. "This one hasn't been checked against
your knowledge base yet — that's the step that makes it a verified source rather than just a
published page."senso destinations list --output table --quiet
selected_for_generation: true means it is in the pipeline. Publishing without
--publisher-ids goes to every selected destination. Name each one it will hit; show the
list only if more than one is selected, or they ask.engine publish is refused. Every
destination, citeables included, lists with selected_for_generation: false, and publishing
without --publisher-ids returns a 400 ("You need at least one destination configured before
you can publish"). Nothing is created. Read the flag, not the presence.citeables: the row whose slug is citeables (display_url
citeables.com) — take its publisher_id and pass it with --publisher-ids. Match on the slug,
not the type: every shared destination lists with type: citeables, and there are eighteen of
them. It is the shared destination the product treats as the default, and the user hears "to
citeables.com" — never picked silently. On the first publish the other domains are offered
beside it, in a clause (below); after that, a custom domain registered since the last publish
is the one thing worth raising.generate update-settings takes
generation, auto-publish, schedule and content type — not publishers — and destinations add
registers a custom domain rather than selecting one. Until a destination is selected in the
app, --publisher-ids goes on every publish. Do not go looking for a select command.live_count and the last-publish timestamp per destination say what is already out there.
Every live_count at 0 means this is the organization's first publish — the ask presents
the destination (below).Every shared destination is a domain Senso runs. citeables.com (slug citeables) is the
default; codeables.dev, cucopilot.com and cited.md sit beside it, and the rest are topic
domains — payroll.md, refinance.md, smbloan.md, precedent.md and others. Any of them
takes a publish by publisher_id, and several at once with --publisher-ids <id> <id>.
A custom destination is a domain the organization owns, run on the same system:
senso destinations add --domain content.example.com --name "Example Citeables" --output json --quiet
Registration is synchronous; the new row appears in destinations list with its own
publisher_id, and the publish passes that. --type defaults to citeables and is the right
default.
Say what citeables.com is, the first time it comes up, in one clause — the user has never heard of it:
citeables.com — a domain Senso runs, not your website: a public URL that AI models can crawl and cite, where every line traces back to your own documents. Nothing on your own site changes.
"What are the others?", "show me the domains" — render from destinations list, one per line,
display_url · name · live pages. No ids, no flag:
- citeables.com — Citeables · 0 pages live
- codeables.dev — Codeables · 0 pages live
- payroll.md — payroll.md · 0 pages live
- …
Or a domain you own — give me the hostname and I'll register it.
They pick by domain; you resolve the id.
Drop the last line inside the Verification Loop. When this list is reached from
senso-verification-loop, show the Senso domains and stop there — the loop never offers
destinations add, because a DNS detour is the one thing that can stall a first run. The loop's
publish step carries the rule; this is the section it applies to.
senso ctas for-content <contentId> --output json --quiet # default | template | none
The published page carries a call-to-action card. for-content returns selection_type:
default (inherit the organization default), template (one pinned to this content), or none.
default does not mean there is a card. On a new organization the selection is default
and there is no default template (ctas list is empty), so it resolves to nothing and the page
publishes with no card. Read selection_type together with ctas list. The one line says "no
call-to-action card" — three words, not the resolution logic.
To pin one for this content only:
senso ctas list --output table --quiet
senso ctas set-for-content <contentId> --selection template --cta-id <ctaId> --output json --quiet
--selection is default, template or none. This is per-content and safe. Changing the
organization default is not — see Fenced off in references/after-publishing.md.
The organization's first publish — every live_count is 0. The destination is a choice they
have never made, so it is presented once, with the ask — unless it was presented and chosen
earlier in this conversation (the Verification Loop does that at the drafting hand-off), in which case this
is an ordinary publish: the one line further down, and go.
"" is ready to publish. It would go to citeables.com, a domain Senso runs — not your website; nothing on your own site changes. It's a public URL that AI models can crawl and cite, where every line traces back to your own documents. No call-to-action card, and it can be unpublished. Senso runs other domains too — codeables.dev, cucopilot.com, cited.md and a set of topic domains like payroll.md and refinance.md — and you can register a domain you own. Publish it to citeables.com, or name another?
Options: citeables.com (Recommended) · Another Senso domain — show me the list · A domain I own. Inside the Verification Loop the third option is not offered.
If they had already said "publish it" before the destination came up, do not ask for the publish again — open on the one thing left to decide: "One thing to settle first — where. It'd go to citeables.com, a domain Senso runs — not your website … citeables.com, or name another?" Same content, one question.
"Publish it", "yes", "go" → citeables.com. A domain named → that one, by its publisher_id. Two
named → both ids, two records. A hostname that is not in the list → destinations add, then
publish to the new id. "Show me the domains" → the list above, then the same question.
Every publish after that — something is live somewhere. It goes where the last one went, and to every destination the last one hit if it hit several. They already said publish:
Publishing "" to citeables.com, where your other pages are — public, can be unpublished. Going.
They approved the draft but have not said publish:
"" would go to citeables.com, where your other pages are — public, can be unpublished. Publish it?
Options: Publish (Recommended) · Not yet.
One sentence, two at most. Name every destination it will hit; if there are three, say three. The
"Going." form runs the command straight after; the others wait. Not the eighteen rows, not
the flag, not the 400, not cache mechanics — those go in the answer if they ask.
senso engine publish --output json --quiet --data '{
"geo_question_id": "<org prompt id>",
"content_id": "<content id>",
"raw_markdown": "<the approved document>",
"seo_title": "<title>",
"summary": "<one line>"
}'
raw_markdown and seo_title are required. geo_question_id is optional in the API but
coming from the flow there is always one — pass it; it is the link back to the prompt.content_id. It ties the publish to the draft that was evaluated rather than minting a
new content item. It is an accepted --data field even though the summary does not list it.--publisher-ids <id> <id> names the destinations. Omit to hit every selected destination —
a 400 if none is, which on a new organization is always.The response carries content_id, version_id, version_num, publish_status,
editorial_status, and publish_destinations[] — one entry per destination:
{ "publisher": "citeables", "display_url": "https://…", "status": "success", "error_msg": "" }
publish_destinations[] and read each status. That is the confirmation — not the
"Content published." line, which printed before the response existed.display_url is the live page — never the builder link. By the time this runs the user has
already been given geo.senso.ai/builder-v2/<content_id>?preview=1, which is where they reviewed
the draft. That link is not the published page and never becomes one; it is an internal
editor view, it is not what a model can crawl, and it stays valid after publishing, so nothing
about it breaking will warn you. The page a model can cite is the one publish_destinations[]
returns. Two URLs now exist for the same content — say the one from the publish response.display_url is the live page. Say it. The close needs it. It is the stable id URL
(…/article/<content_id>) and redirects (308) to a slug URL; content verification reports
the slug as external_url. Both resolve. Cite the id URL — it does not change if the title does.publish_destinations may be absent entirely — the key is left out, rather than set to an
empty list, when there is nothing to report. Absent is a finding, not a pass.version_num is one higher than the draft you
sent — a publish is itself a new version of the content item, so content versions shows the
approved draft and the published copy above it. Not a duplicate.editorial_status flips to published when any one destination succeeds, so it can read
published while another destination failed. Read the records.Report it per destination, in words:
Live on citeables at https://… — that's the URL models will cite. The acme.md publish failed (
error_msg: …); I can retry that one on its own.
Everything after engine publish has run — retrying a failed record, pulling a page back, recording
a publish made elsewhere, citations and which prompts a page is winning, the provenance audit, and
the live-page settings that are fenced off — is in references/after-publishing.md. Read it
before running any command in that territory. Without it: publish-records retry on a record that
did not fail, a page pulled back with no confirmation shown first, and a ctas or destinations
command run as tidying that rewrites a page already public.
No writing or checking. Drafting, editing and evaluating are senso-generate-verify's; a draft
arrives here already approved.
No knowledge base work. senso-context-layer.
| They want | Skill |
|---|---|
| Save, find or organize a document | senso-context-layer |
| An industry, the brand name, the segment | senso-verification-loop-setup |
| Where they stand, the next question they are missing from | senso-verification-loop |
| The gap report | senso-gap-report |
| Draft, revise or evaluate content | senso-generate-verify |
| To get started with Senso | senso-quickstart |
This is the end of the round. Say what is live, where, what to watch — and offer the next gap:
"" is live at https://… . It typically takes a few days for agents to pick it up — I'll be able to show you which prompts it's winning once that starts. To publish anything else, draft it and say "publish this". Or say "fill another gap" and I'll find the next question your industry gets asked that your knowledge base can't answer yet.
When the organization has its own velocity number, use it in place of "a few days".
The last sentence is the loop. It hands to senso-verification-loop, which finds the next
question the brand is missing from, closes it, and brings a new source back through
generate-and-verify to here. Offer it once; take no for an answer.
references/troubleshooting.md — publish records, destinations, CTAs, and what a failed publish
actually means