How to Build an OKF Bundle: A Step-by-Step Guide to Google's Open Knowledge Format
The hands-on companion to the OKF-vs-RAG question โ writing a spec-conformant bundle under the current v0.2 rules, without needing Google's BigQuery-specific reference agent.
Written as the hands-on companion to this site's own OKF vs RAG analysis, built directly from the spec's current v0.2 conformance rules and its GitHub quickstart docs rather than the BigQuery-specific reference agent Google ships alongside it.
An Open Knowledge Format (OKF) bundle is a directory of markdown files with YAML frontmatter, and the only mandatory field is `type`. This guide walks through building one from scratch under the current v0.2 spec: writing a valid concept file, adding the new trust and lifecycle fields, cross-linking concepts into a graph, validating conformance, and wiring it into an agent.
1. The Bundle Structure You're Building Toward
This guide assumes the "why" from OKF vs RAG โ that Google's Open Knowledge Format (OKF) solves deterministic lookup of known facts, not fuzzy retrieval โ and focuses entirely on the "how." If you haven't decided whether OKF is the right tool for what you're building, start there first.
An OKF bundle is nothing more than a directory of markdown files. Here's a small, real one for a payments team's on-call knowledge, the kind of thing that would otherwise live scattered across a wiki, a runbook doc, and someone's memory:
payments/โโโ index.mdโโโ log.mdโโโ metrics/โ โโโ failed_payment_rate.mdโโโ runbooks/ โโโ stripe_webhook_outage.mdTwo file names are reserved: index.md (a directory listing, for progressive disclosure) and log.md (update history for that scope). Every other markdown file is a concept โ one knowledge artifact per file. The rest of this guide builds the two concept files above from nothing to spec-conformant.
2. Step 1: Write a Concept File With the One Required Field
Per the spec's conformance criteria, a concept file needs exactly one thing: a YAML frontmatter block with a non-empty type field. This is, in full, a valid OKF concept:
---type: Incident Runbook---That's conformant. It's also nearly useless to anyone reading it. The recommended fields exist precisely to close that gap โ title, description, resource, and tags are all optional per the spec, but they're what make a concept worth an agent's or a human's time to open:
---type: Incident Runbooktitle: Stripe Webhook Outagedescription: What to do when Stripe webhook deliveries stop arriving.resource: https://runbooks.internal/payments/stripe-webhook-outagetags: [payments, stripe, on-call]timestamp: 2026-07-20T09:00:00Z---# SymptomsPayment status stays "pending" past 2 minutes. No errors in the app logs โthe charge succeeded on Stripe's side, but the webhook that should mark itpaid never arrived.# First checks1. Check Stripe's [status page](https://status.stripe.com) for webhook delays.2. Confirm the webhook endpoint is returning 200s, not silently 5xx-ing.3. Replay missed events from the Stripe dashboard once the endpoint is healthy.Nothing here requires a schema registry or a Google Cloud account to write or validate. It renders correctly on GitHub as-is, and any consumer that can read a file and parse YAML frontmatter can use it.
3. Step 2: Add v0.2's Trust and Lifecycle Fields
The spec moved from v0.1 to v0.2 within months of its initial release, and the additions are specifically the trust and lifecycle fields v0.1 didn't have โ this is the part of the spec that's easiest to find outdated coverage of, because most of what's been written about OKF still describes v0.1.
Five fields matter here, all optional, all worth using on concepts where an agent needs to judge freshness or trust before treating something as ground truth:
- `status` โ one of
draft,stable, ordeprecated. An agent reading adeprecatedrunbook should say so, not follow it silently. - `stale_after` โ an absolute
YYYY-MM-DDdate. Past this date, the concept is a candidate for review, not automatically wrong โ the spec doesn't mandate any behavior on expiry, it just gives a consumer the date to reason about. - `generated` โ a
{ by, at }object recording who or what produced the file and when. - `verified` โ a list of
{ by, at }entries; a bare mapping (not wrapped in a list) is explicitly valid too, since the spec requires consumers to treat a singleverifiedentry as a one-element list rather than rejecting it. - `sources` โ an array of credibility signals (
author,usage_count,last_modified) an agent can weigh when a concept was assembled from more than one place.
Applied to the runbook:
---type: Incident Runbooktitle: Stripe Webhook Outagestatus: stablestale_after: 2026-10-01generated: by: platform-team at: 2026-07-20T09:00:00Zverified: by: on-call-lead at: 2026-07-22T14:00:00Z---None of this is required to be spec-conformant. It's the difference between an agent that can only say *what* a runbook claims and one that can also say *how much to trust it right now*.
5. Step 4: Validate the One Rule That Actually Matters
Almost every conformance requirement in the spec is aimed at *consumers* โ tolerate unknown types, tolerate missing optional fields, tolerate broken links, tolerate a missing index.md. The one requirement aimed at *producers* is the type field. A minimal validator only needs to check that:
import { readFileSync, readdirSync, statSync } from "node:fs";import { join } from "node:path";function checkConcept(filePath) { const text = readFileSync(filePath, "utf8"); const match = text.match(/^---\n([\s\S]*?)\n---/); if (!match) return { filePath, ok: false, reason: "no frontmatter block" }; const typeMatch = match[1].match(/^type:\s*(.+)$/m); const type = typeMatch?.[1]?.trim(); if (!type) return { filePath, ok: false, reason: "missing or empty type field" }; return { filePath, ok: true };}function walk(dir) { const results = []; for (const entry of readdirSync(dir)) { if (entry === "index.md" || entry === "log.md") continue; // reserved, not concepts const fullPath = join(dir, entry); if (statSync(fullPath).isDirectory()) results.push(...walk(fullPath)); else if (entry.endsWith(".md")) results.push(checkConcept(fullPath)); } return results;}const failures = walk("./payments").filter((r) => !r.ok);if (failures.length) { console.error(`${failures.length} concept file(s) failed conformance:`); failures.forEach((f) => console.error(` ${f.filePath}: ${f.reason}`)); process.exit(1);}console.log("All concept files are OKF-conformant.");This deliberately checks nothing else โ no schema on type values, no requirement that links resolve, no requirement that recommended fields exist โ because a stricter check would itself be non-conformant with what the spec requires consumers to tolerate. Google's own repository ships a more thorough reference agent alongside the spec, but it targets BigQuery and Gemini specifically; this check works on any bundle, from any source, with zero dependencies.
6. Step 5: Give an Agent Two Ways to Read the Bundle
The hybrid-architecture point from the companion post becomes concrete here: an agent needs two different tools, not one retrieval pipeline that tries to serve both.
Direct lookup, for when the agent already knows what it's asking for โ a table schema, a specific runbook:
async function getConcept(path: string): Promise<{ frontmatter: Record<string, unknown>; body: string }> { const text = await fs.readFile(path, "utf8"); const [, frontmatterBlock, body] = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) ?? []; return { frontmatter: parseYaml(frontmatterBlock ?? ""), body: body ?? "" };}Graph walk, for when the agent is exploring โ "what do we know about payments incidents":
async function listConcepts(dir: string): Promise<string[]> { const indexPath = path.join(dir, "index.md"); const exists = await fs.access(indexPath).then(() => true).catch(() => false); return exists ? extractLinks(await fs.readFile(indexPath, "utf8")) : await fs.readdir(dir);}Neither of these is a similarity search, and that's the point: there's nothing to rank. The agent asked for payments/runbooks/stripe_webhook_outage.md and got exactly that file, with a status and stale_after it can reason about before trusting it. Everything genuinely unstructured โ support tickets, chat logs, freeform docs โ keeps going through whatever RAG pipeline you already have. This bundle doesn't replace it; it just stops those two problems from being solved with the same tool.
Frequently Asked Questions
Do I need Google Cloud, BigQuery, or Gemini to use OKF?
No. The format itself has zero required tooling โ it's markdown files with YAML frontmatter, readable and writable with a text editor. Google's own reference implementation happens to target BigQuery metadata and Gemini for web enrichment, but that's one producer, not a requirement of the spec. Any script, in any language, that can write a text file with a type field in its frontmatter produces a conformant concept.
What happens if I get a concept file's frontmatter wrong?
It depends what 'wrong' means. Missing optional fields, unknown type values, unknown extra frontmatter keys, and links to files that don't exist are all explicitly things a conformant consumer must tolerate, not reject. The only failure that actually breaks conformance is a missing or empty type field, or a frontmatter block that doesn't parse as valid YAML at all.
Do I need index.md and log.md in every directory?
No โ both are optional, and the spec explicitly states consumers must not reject a bundle for a missing index.md. Add them once a directory has enough concepts that a listing or a change history genuinely helps navigation; a small bundle doesn't need either.