Build an Idempotent KB Push Script
To push articles to the KB safely and repeatably, your script should still supply the idempotency itself: list existing articles by slug first, PATCH the ones that already exist, and POST only the ones that don’t. The API accepts slug on create and update, so when your script sends the frontmatter slug, the slug you write is the slug you get. If a live article already uses that slug, POST /api/v1/kb/articles returns 409 and writes nothing. Only a slug derived from the title (because you didn’t send one) gets a -2/-3 suffix when it collides.
This article covers the upsert logic, the requirements checklist, and the frontmatter contract that a doc-as-code push script needs. For when to use the pattern at all, see Doc-as-code with the KB API.
The upsert logic
Idempotency in doc-as-code means “running the script twice with the same inputs produces the same outputs.” The script achieves it by:
- Listing existing articles by slug before pushing.
- If a slug matches an existing article,
PATCHit instead ofPOST. - If the slug doesn’t match anything,
POSTto create.
This pattern is robust against partial runs — a script that crashes halfway through can re-run, find the articles it already created, and update them on the second pass instead of creating duplicates.
A 409 on re-run is a second, safer signal that the article already exists: nothing was written, so nothing is duplicated. Don’t rely on it alone, though. The lookup-then-PATCH flow is still the recommended approach, because it updates the article instead of just failing.
See What happens when I push the same article twice? for the exact behavior.
What the script needs
- Authentication — A tenant API key with
knowledge:read(for listing) andknowledge:write(for create/update). Generate in Settings → API & MCP. - Frontmatter parser — Any Markdown frontmatter library. The script reads
title,slug,summary,category, etc. from the frontmatter and sends them to the API. - Categories upserted before articles — Articles fail to create without an existing category. The script must ensure categories first, collect IDs, then push articles.
- Slug-based lookup — List articles, build a slug → ID map, decide create vs. update per article.
- Markdown-to-HTML conversion — The API accepts Markdown in
content; the renderer handles the HTML conversion server-side. No client-side conversion required.
The Atender vault uses a 600-line Python script that does all of this in one pass. It’s a useful reference but the same shape is easy to build in any language.
Frontmatter as metadata contract
The piece that makes doc-as-code workable is the frontmatter contract. Every article carries metadata at the top:
---
title: "How to update your payment method"
slug: "update-payment-method"
category: "Billing"
type: "how-to"
summary: "Open Account, go to Billing, click Change."
keywords: [payment, credit card, billing]
ux_path: "Account → Billing → Payment methods"
roles: [admin]
status: "published"
---
The push script reads this and translates it into the API’s payload shape. The script becomes a thin transformation layer between the human-friendly frontmatter and the API’s JSON.
If you send tags, the API accepts either tag ids or tag slugs. An unknown tag returns a 400 and nothing is changed, so a typo in a tag fails the whole request instead of leaving a half-updated article.
Verify it worked
Re-run the script with no source changes: it should report updates (or no-ops), not create any new articles. If your script sends the frontmatter slug, a re-run that wrongly tries to POST an existing article gets a 409 and writes nothing, so treat any 409 in the output as a sign your slug lookup missed an article. The live KB should show no -2/-3 slug duplicates. Then check the pushed articles render correctly in the KB editor.