Skip to main content

blog.publish

The "publish a blog post" pack. Two destinations, two body modes, two formats — picked at call time. Closes #68.

AxisOptions
Destinationghost (live publish via Ghost Admin API) · artifact (render to a helmdeck artifact, no external network)
Body modebody (the agent already wrote the post) · prompt + model (the pack expands the prompt into a body via the gateway LLM)
Formatmarkdown (rendered to HTML via goldmark when Ghost destination needs it) · html (pre-rendered, passes through)

The two body modes let an agent treat publishing as either a primitive (it composed the body upstream and just hands it off) or as a macro (it knows what it wants but lets the pack do the writing). The two destinations let the same agent publish a draft to a real Ghost blog OR generate a stand-alone artifact a downstream system can pick up.

Setup prerequisite

For the ghost destination, add the Ghost Admin API key to the Vault panel:

FieldValue
Nameghost-admin-key (exact string — pack default; override with credential input)
Typeapi_key
Host patternYour Ghost installation's hostname (e.g. blog.example.com)
ValueThe full Admin API key in <id>:<secret> form (Ghost ships them this way; secret is hex-encoded)

Get the key from your Ghost admin: Settings → Advanced → Integrations → Add custom integration → Admin API Key. The key looks like 650f...:a1b2c3... — paste the whole thing, including the colon.

For the artifact destination, no vault credential is needed — the pack writes locally to the helmdeck artifact store.

Inputs

FieldTypeRequiredDefaultNotes
destinationstringyes"ghost" or "artifact".
formatstringyes"markdown" or "html".
titlestringyesPost title. Slugified for the artifact filename.
bodystringone-ofThe post body. Either this or prompt+model.
promptstringone-ofGeneration prompt for prompt mode. Either this or body.
modelstringwith promptProvider/model for prompt mode (e.g. openrouter/openai/gpt-4o-mini).
max_tokensnumberno1024Cap on the prompt-mode body length. Ignored in body mode.
tagsarrayno[]Tag names. For Ghost, converted to {name: ...} objects.
statusstringno"draft""draft" (default), "published", or "scheduled".
published_atstringwith status="scheduled"RFC3339 timestamp in the future.
hoststringwith destination="ghost"Ghost installation hostname. Accepts host, https://host, or http://host:port (the last for self-hosted Ghost on a non-HTTPS port).
credentialstringno"ghost-admin-key"Vault credential name. Override only if you store the key under a non-default name.

Validation:

  • Exactly one of body or (prompt+model) — providing both or neither errors.
  • status="scheduled" requires published_at in the future.
  • destination="ghost" requires host and a vault credential.

Outputs

Common fields:

FieldTypeNotes
destinationstringEcho.
formatstringEcho.
body_sourcestring"input" (body mode) or "model" (prompt mode).
model_usedstringOnly in prompt mode — the model that generated the body.

Ghost-specific:

FieldTypeNotes
post_idstringGhost post id.
urlstringPublic URL.
html_urlstringSame as url, for parity with github.* packs.
statusstringGhost-confirmed status.
published_atstringGhost-assigned RFC3339.

Artifact-specific:

FieldTypeNotes
artifact_keystringblog.publish/<slug>.{md|html}. Resolve via /api/v1/artifacts/<key>.
sizenumberBytes.

Vault credentials needed

ghost-admin-key for ghost destination only. Optional for artifact destination.

Use it from your agent (OpenClaw chat-UI worked example)

OpenClaw chat capture pending.

Developer reference (curl)

Artifact mode (no Ghost required)

ADMIN_PW=$(grep HELMDECK_ADMIN_PASSWORD /root/helmdeck/deploy/compose/.env.local | cut -d= -f2)
JWT=$(curl -fsS -X POST http://localhost:3000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"admin\",\"password\":\"${ADMIN_PW}\"}" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')

curl -fsS -X POST http://localhost:3000/api/v1/packs/blog.publish \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{
"destination": "artifact",
"format": "markdown",
"title": "Demo PR-D2 post",
"body": "# Hello\n\nThis is a test."
}'

Response:

{
"pack": "blog.publish",
"version": "v1",
"output": {
"destination": "artifact",
"format": "markdown",
"body_source": "input",
"artifact_key": "blog.publish/demo-pr-d2-post.md",
"size": 101
}
}

Ghost mode (live API)

curl -fsS -X POST http://localhost:3000/api/v1/packs/blog.publish \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{
"destination": "ghost",
"format": "markdown",
"title": "Hello from helmdeck",
"body": "# Welcome\n\nThis post was filed via blog.publish.",
"host": "blog.example.com",
"tags": ["demo","helmdeck"],
"status": "draft"
}'

Response:

{
"pack": "blog.publish",
"version": "v1",
"output": {
"destination": "ghost",
"format": "markdown",
"body_source": "input",
"post_id": "650f1234567890",
"url": "https://blog.example.com/p/hello-from-helmdeck/",
"html_url": "https://blog.example.com/p/hello-from-helmdeck/",
"status": "draft",
"published_at": null
}
}

Prompt mode + Ghost

curl -fsS -X POST http://localhost:3000/api/v1/packs/blog.publish \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{
"destination": "ghost",
"format": "markdown",
"title": "Why packs beat naive function-calling",
"prompt": "Write a 400-word post arguing that typed packs (helmdeck) yield 10x lower per-task LLM cost than naive function-calling on Sonnet. Use a concrete example.",
"model": "openrouter/openai/gpt-4o-mini",
"max_tokens": 600,
"host": "blog.example.com",
"status": "draft",
"tags": ["agent-architecture","cost"]
}'

The pack calls the gateway LLM with a frozen system prompt that instructs it to emit ONLY the post body in the requested format (no preamble, no surrounding code fences, no repeated title).

Error codes

CodeTriggersCaptured response
invalid_inputdestination outside "ghost"/"artifact"destination must be "ghost" or "artifact"
invalid_inputformat outside "markdown"/"html"format must be "markdown" or "html"
invalid_inputtitle emptytitle is required
invalid_inputBoth body AND prompt suppliedmust provide either body OR prompt+model, not both
invalid_inputNeither body nor prompt suppliedmust provide either body OR prompt+model
invalid_inputprompt set but model missingprompt mode requires model (provider/model)
invalid_inputstatus outside the closed setstatus must be "draft", "published", or "scheduled"
invalid_inputstatus="scheduled" without published_atpublished_at (RFC3339) is required when status=scheduled
invalid_inputpublished_at not in the futurepublished_at must be in the future for status=scheduled
invalid_inputdestination="ghost" without hosthost is required when destination=ghost
invalid_inputghost-admin-key not in vaultvault credential "ghost-admin-key" not found …
invalid_inputVault key not in id:hex_secret formghost-admin-key vault value must be \:` …`
invalid_inputGhost host resolves to a blocked rangeegress denied: …
internalPrompt mode but pack registered without a gateway dispatcherblog.publish prompt mode registered without a gateway dispatcher
handler_failedGhost API non-2xxghost API POST …: 401 Authorization failed
handler_failedMarkdown→HTML conversion failedmarkdown→html for Ghost: …
handler_failedPrompt expansion model returned no choicesblog.publish prompt expansion: model returned no choices
artifact_failedObject store write failedartifact upload failed: …

Session chaining

No session. Stateless. Composes naturally:

  • research.deepcontent.groundblog.publish — the canonical "evidence-grounded blog post" chain. Research surfaces sources; content.ground appends citations into a draft body; blog.publish ships it.
  • web.scrapeblog.publish (artifact mode) — re-publish a scraped page as a draft artifact for later editing.
  • repo.fetch + fs.read + blog.publish (prompt mode) — generate a release-notes blog post from a repo's recent changelog without the agent ever materializing the body itself.

Async behavior

Synchronous. Wall-clock = (prompt-mode LLM call, ~3–10s if used) + (markdown→html via goldmark, ~1ms) + (Ghost API round-trip, ~200–800ms) for ghost mode; or just the goldmark step + artifact upload for artifact mode (~10–50ms).

See also