Skip to main content

Overview

LLMs can misquote Scripture when they recite it from memory: a word substituted, a phrase dropped, a verse attributed to the wrong reference. This recipe uses the output transform pattern to remove that risk from the text that matters most: instead of asking the model to quote Scripture, you ask it to emit a reference placeholder like {{SCRIPTURE:John 3:16}}, then your own code deterministically fetches and substitutes the real verse text from a Bible API. Every successfully substituted Scripture placeholder contains the text returned by the selected Bible API. The same technique also gives you translation control that’s independent of theological framing. Gloo’s tradition parameter shapes theological perspective, and as part of that, it applies the King James Version as its scripture quoting standard. If you want a different translation (BSB, NIV, ESV, NASB, or any licensed text), output transforms let you choose it directly, without depending on tradition or on how the model quotes from memory.
The placeholder instruction is a system prompt, not a hard constraint. A model could ignore it and include memorized Scripture directly in its commentary. The output transform verifies substituted quotations; it does not verify every piece of model-generated commentary. See Strengthen This Pattern for ways to close that gap further.
By the end of this recipe, you’ll have a CLI that streams a response from the Gloo AI Guarded Responses API, detects {{SCRIPTURE:reference}} placeholders as they arrive, and substitutes real verse text from the Free Use Bible API, all without waiting for the full response to complete.

Prerequisites

Before starting, ensure you have:
  • A Gloo AI Studio account and an API key
  • Authentication setup: complete the Authentication Tutorial first
  • No account needed for the Bible API. Free Use Bible API is free and keyless
  • Python 3.9+, Node.js 18+, PHP 8.1+, Go 1.20+, or Java 17+ with Maven, depending on which language you’re using

Working Code Sample

Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java).Setup and testing instructions are provided later.

Why Output Transforms?

Quoting a passage involves two separate decisions: which passage is relevant, and what that passage says. Output transform splits them. The model makes the first decision, which stays probabilistic, and emits a reference. Your code makes the second by fetching the text from a Bible API and inserting it after the model has finished. No model runs after the verse text is retrieved, so nothing downstream can reword it. This isn’t a Scripture-specific trick. Having a model write abstract placeholders that tools later fill in with concrete values is an established pattern in LLM research, used to decouple a model’s general reasoning from the domain-specific values that tools supply.1 For Scripture quotation specifically, a published evaluation of several quotation methods found output transform the most reliable of those tested.2 That’s an observed result from one evaluation, not a universal guarantee about every model or every prompt; treat it as evidence for a design choice, not a proof. Two alternatives are worth naming:
  • Tool calls, where the model calls a get_verse function and receives the text back in a second round trip before writing its response. The comparison below covers the tradeoffs in detail.
  • RAG (retrieval-augmented generation), where verse text is retrieved and stuffed into the prompt context ahead of generation. This helps the model reference the right passage, but like tool calls, the model is still the one writing out the quoted text, and nothing enforces that what it writes matches what was retrieved.
Output transform sidesteps both problems for the specific case of Scripture quotation: when the model follows the placeholder instruction, it doesn’t see or write the verse text at all. It only writes a reference, and your code does the rest. That’s a property of the design, not an enforced guarantee; see the note in the Overview for what happens when a model doesn’t comply.

Output Transforms vs Tool Calls

Here are some things to consider when considering output transform compared to tool calling. The key row is the first one: every generative step placed after retrieval is another chance for the quoted text to change. That matches the evaluation cited above, where output transform outperformed tool calls for exact quotation. If your use case genuinely needs the model to reason about verse content before responding (summarizing a passage, comparing two verses, answering a question that depends on what a verse actually says), tool calls are the better fit, even with their added complexity. The Completions with Tool Use tutorial covers that pattern in full.

Step 1: Define the System Prompt

The system prompt is the only thing standing between the model and quoting from memory. It tells the model to emit a placeholder instead of text, and gives it the exact format to use.

Key Points

  • This is the only instruction shaping the model’s behavior toward Scripture: no schema, no tool definition. That simplicity is part of the appeal of output transform.
  • The prompt explicitly permits commentary and explanation; it only restricts direct quotation.
  • The last two sentences control where placeholders go. The verse is substituted as a full sentence with its citation, so a placeholder used as part of a sentence ({{SCRIPTURE:John 3:16}} shows...) or inside parentheses produces awkward output. Asking for the verse to stand on its own avoids that.
  • In Step 4, this prompt is sent as the Responses instructions field.
  • Because this is a system prompt and not an enforced constraint, a non-compliant response is possible. The buffer in Step 5 only substitutes what it recognizes as a placeholder. It can’t detect a quotation the model wrote out directly.

Step 2: Write the Bible API Fetch Function

This recipe uses the Free Use Bible API which is chapter-based, not verse-based. You fetch an entire chapter as JSON and pull out the verse(s) you need. The fetch function has three jobs: parse a human-readable reference like "Romans 8:28-30" into a book ID, chapter, and verse list; call the API; and extract the matching verse text from the chapter’s structured content. Book names arrive from model output, so the lookup has to tolerate variation: "1 John", "I John", "1John" should all resolve. The full mapping covers all 66 Protestant canon books plus common variants (abbreviated below for readability; see the cookbook for the complete table).

Key Points

  • The API is chapter-based, so a single-verse request still fetches and filters an entire chapter.
  • parseReference (or its equivalent) handles ranges ("Romans 8:28-30") by returning a list of verse numbers. A bare chapter reference ("Psalm 23") returns an empty list, which the fetch function treats as “return the whole chapter.”
  • extractText walks the API’s nested content structure recursively. Verse content can include footnotes and line breaks as nested objects, and this filters down to plain text.
  • If a verse isn’t found in the response, the function raises rather than silently returning partial or empty text. A missing verse should surface as an error, not corrupted output.

Step 3: Set Up Authentication

Load GLOO_API_KEY from the environment and send it as a bearer token on every request. See the Authentication Tutorial for the full setup, including where to find your key in Gloo AI Studio.

Step 4: Send the Streaming Request

Send the request to the Guarded Responses API (POST /ai/v2/guarded/responses) with these key inputs:
  • The system prompt goes in instructions.
  • The user’s prompt goes in input.
  • Set stream: true to return Server-Sent Events.
  • Omit tradition to start off. See the Troubleshooting section for why.
Responses takes a pinned model. This recipe uses gloo-openai-gpt-6-luna, a fast, low-cost model, set once in a MODEL constant so it’s easy to change. Any generation model from Supported Models works; see Using a Different Model. Each streamed event arrives as an event: line followed by a data: line. Text arrives in response.output_text.delta events, response.completed marks the end, and the other response.* events are lifecycle events you can ignore. Treat any other event, such as error, as the end of the stream.

Key Points

  • There’s no [DONE] sentinel and no choices[0].delta.content as in Chat Completions. Text comes only from response.output_text.delta, and the final response.completed event carries token usage, not the text.
  • Responses doesn’t support auto_routing or model_family; those are Completions V2 features. Pinning a model keeps behavior predictable for a recipe whose output depends on the model following the placeholder instruction.
  • If the API rejects the request (for example, an unknown model ID), the cookbook prints the API’s own reason from the error body, such as Unknown model '...'.

Step 5: Buffer and Substitute Placeholders

This is the core of the output transform. SSE chunks can split anywhere, including in the middle of {{SCRIPTURE:. A naive scan-and-replace on each chunk would miss placeholders split across chunk boundaries, so the implementation accumulates text in a buffer and only emits it once it’s sure it isn’t part of a placeholder still arriving. The algorithm, per text delta:
  1. Append the delta to the buffer.
  2. While the buffer contains a complete {{SCRIPTURE:reference}} match: emit everything before it, fetch the verse, emit the verse text (or leave the placeholder visible if the fetch fails), and drop the consumed text from the buffer.
  3. If the substituted verse already ends its sentence and the model wrote a . right after the placeholder, drop that period so the output doesn’t read ...through Him. (John 3:17, BSB). The period can arrive in a later delta, so the check carries over until the next character arrives.
  4. Check whether the buffer’s tail could still become {{SCRIPTURE:, including a lone trailing {. If so, hold that tail back; otherwise emit the whole remaining buffer and clear it.
  5. After the stream ends, emit whatever is left in the buffer. An incomplete placeholder at that point is shown as-is: a visible reference, not corrupted text.

Key Points

  • On a failed fetch, the placeholder is left visible ({{SCRIPTURE:reference}}) rather than replaced with empty or invented text. A visible failure beats a silent one.
  • The find_incomplete_prefix (or language-equivalent) check is what makes this work across arbitrary SSE chunk boundaries. Without it, a chunk split as ...{{SCRI / PTURE:John 3:16}}... would stream {{SCRI to the terminal and never substitute the placeholder.
  • The redundant-period check only drops a single ., and only after a successful substitution whose verse already ends in ., !, or ?. Commas and other punctuation are kept, and so is the period after a placeholder left visible by a failed fetch.
  • Nothing here inspects the model’s commentary for direct quotations. It only recognizes and substitutes well-formed placeholders. See Strengthen This Pattern for closing that gap.

Step 6: Run the CLI

Every language accepts the same flags: a required positional prompt, an optional --translation (default BSB), and an optional --tradition (omitted by default; see Troubleshooting).

Example 1: Single Verse

The model writes three paragraphs of commentary and emits one placeholder; the CLI substitutes it with BSB text as the response streams:
Wording will vary between runs since the model generates fresh commentary each time, but the substituted verse text is fixed: every run against the same translation reproduces the same BSB text for John 3:16.

Example 2: Model-Selected Verses

This prompt doesn’t name a verse. The model has to find one that answers the question on its own, and in this run it chose John 3:17:
This is the point of the pattern: you don’t need to know in advance which verses a prompt will touch. The model selects them, and the buffer in Step 5 substitutes however many placeholders arrive, one after another.

Customizing the Recipe

Using a Different Model

The recipe pins gloo-openai-gpt-6-luna in a single MODEL constant (model in Go, Main.MODEL in Java). To use a different model, change that constant to any generation model ID from Supported Models. After switching, re-run both examples. How reliably a model follows the placeholder instruction, and where it puts placeholders, varies from model to model, and the recipe’s output depends on both.

Using a Licensed Translation

The Free Use Bible API only serves public-domain and freely-licensed translations, like BSB and WEB. For a licensed translation (NIV, ESV, NASB, NLT, and others), replace the body of fetch_bible_verse (or its per-language equivalent) with a call to a licensed provider’s API, keeping the same function signature: accept a reference and translation ID, return formatted verse text. Everything else in this recipe (the system prompt, the buffer, the substitution loop) is provider-agnostic and doesn’t need to change. Concrete options include the YouVersion Platform and API.Bible: sign up for API credentials, then point fetch_bible_verse at the provider’s API instead of Free Use Bible API. Licensed providers typically return copyright or attribution information with each passage. Take it from the API response and display it alongside the verse, rather than letting the model generate it.

Adding Theological Framing

If you want evangelical, Catholic, or mainline framing but need to keep translation control, add the framing directly to your system prompt instead of setting tradition:
This gives you theological framing in the commentary while keeping the placeholder mechanism, and therefore your chosen translation, intact.

Strengthen This Pattern for Higher-Assurance Applications

The output transform verifies substituted quotations. It does not verify every piece of model-generated commentary. A model that ignores the system prompt could still include a memorized (and possibly inaccurate) quotation directly in its commentary, where the buffer has no way to detect it. If your application needs a stronger guarantee than “every successfully substituted placeholder is verified,” here are four ways to close that gap, roughly in order of effort: None of these are implemented in this recipe. They’re documented here as a starting point if your application needs more than what output transform guarantees on its own.

Validate Before Displaying, and Fail Closed

Buffer the complete response instead of streaming it straight to the user, run a check, and only then render it. Treat the response as invalid (and show an error or retry, rather than displaying it) if a placeholder survives to the end of the stream unsubstituted, or if the commentary contains text that looks like a verbatim quotation. This trades away the live-streaming behavior this recipe demonstrates in exchange for the chance to inspect the full response before anything reaches the user. Whatever you do on failure, don’t fall back to asking the model to quote the verse itself when the Bible API is unavailable. That silently swaps an API-sourced quotation for a memorized one, which is the exact risk this pattern exists to remove.

Compare Commentary Against the Translation

Search for exact or near-exact matches between the model’s commentary and the selected translation’s text, and reject responses where one is found. A practical version of this needs:
  • Access to the complete selected translation, not just the verses referenced this turn.
  • Normalization of punctuation, capitalization, and whitespace before comparing.
  • A searchable n-gram index, with a defined threshold for what counts as a quotation (for example, six or more consecutive matching words).
  • Fail-closed behavior when a match is found.
  • Testing against short, commonly repeated phrases to check for false positives.
Even fully built, this only catches exact or near-exact matches under whatever threshold you choose, not every possible paraphrase.

Use Structured Output with Typed Segments

Structured output can enforce a response’s shape; it can’t by itself prove commentary contains no memorized Scripture. That distinction matters for what this approach can and can’t promise. Instead of streaming free text with inline placeholders, have the model return a single JSON document with typed segments:
A strict JSON schema (passed via response_format, if your target model and endpoint support it) can enforce that every segment has a valid kind and value. The client then buffers the full response, parses and validates it, rejects unknown fields or malformed references, fetches every scripture segment from the Bible API, and renders only the fetched text as a quotation, failing closed if validation or retrieval fails. Check that your pinned model supports it, since structured-output support and behavior vary by provider. This is a real guarantee, but a narrower one than it might first appear:
Segments designated as Scripture quotations are deterministically sourced from the selected Bible API; invalid or unfetched references are never rendered as quotations.
It does not guarantee that Scripture text can’t appear inside a commentary segment. {"kind": "commentary", "value": "For God so loved the world..."} is structurally valid despite containing a memorized quotation. Closing that gap requires layering the commentary-comparison approach above on top. It also changes the streaming behavior this recipe demonstrates: because the full JSON must be validated before anything is safe to render, this trades live token-by-token streaming for a single validated response that can only be rendered progressively at display time, not generation time. That tradeoff, plus the added schema validation and model-compatibility handling, is why this recipe doesn’t implement it: the complexity is real, and it doesn’t fully close the underlying semantic risk on its own.

Visually Distinguish API-Sourced Quotations

In a UI, render substituted verse text with a distinct style, such as a citation card or a different background, so users can see at a glance which words came from the Bible API and which came from the model’s own commentary. This doesn’t prevent a non-compliant response; it makes the distinction legible to the reader when one occurs.

Working Code Sample

View Complete Code

Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions and environment configuration.
The code snippets in this tutorial are simplified and self-contained. The book-name mapping is abbreviated for readability, and the logic that’s split across multiple files in PHP, Go, and Java (BibleApi, SseStream, SubstitutionStream, and similar) is shown inline here. The cookbook examples include the full 66-book mapping and the production file layout. Both implement the same APIs and patterns.

Next Steps

Now that you’ve completed this recipe, you can:

Troubleshooting

The model quotes Scripture directly instead of using a placeholder

Cause: The system prompt is a soft instruction, not an enforced constraint. Some prompts or models are more prone to ignoring it, and setting tradition adds instructions of its own (see below). Solution: Check your system prompt is being sent on every request (not just the first, if you’re maintaining conversation history). If direct quotation persists, add one of the checks from Strengthen This Pattern, such as a post-response check for verbatim text matching the translation.

Placeholder appears in the output unsubstituted, e.g. {{SCRIPTURE:John 3:16}}

Cause: The Bible API fetch failed, usually because of an unknown book name, a bad chapter/verse number, or a network error. This is the intended graceful-failure behavior: the code leaves the placeholder visible rather than guessing. Solution: Check stderr for a [Bible API error for ...] message with the underlying cause. If it’s an unknown book name, extend the BOOK_IDS mapping with the variant the model used.

The API returns HTTP 400 for an unknown model

Cause: The MODEL constant names a model ID that doesn’t exist or is no longer offered. The CLI prints the API’s message, for example Error: Gloo AI API returned HTTP 400: Unknown model '...'. Solution: Pick a current generation model ID from Supported Models and update MODEL. Models marked deprecated keep working for a time but should be replaced.

The verse reads awkwardly inside a sentence

Cause: The model used a placeholder as part of a sentence or inside parentheses, so the full verse and its citation landed mid-sentence. Solution: Make sure the system prompt includes the placement sentences from Step 1 (“Place each placeholder where the full verse can stand on its own…”). If you’ve customized the prompt, keep those sentences.

”Unknown book name” error

Cause: The model referenced a book name or abbreviation not present in the lookup table. Solution: Add the missing variant to BOOK_IDS. The cookbook’s mapping already covers common forms ("1 John", "I John", "1John", "First John"), but models can still produce unexpected phrasing.

Streaming output looks corrupted or a placeholder gets split across lines

Cause: Usually a symptom of bypassing the buffer, for example by printing SSE content deltas directly instead of routing them through stream_with_substitution (or the language equivalent). Solution: Confirm every content delta flows through the buffering function before being written to output. Direct prints will occasionally split a {{SCRIPTURE:...}} marker across two writes.

The tradition parameter may produce KJV-flavored commentary

Cause: Setting tradition adds Gloo’s own values-alignment instructions to the request, including the King James Version as its quoting standard. Placeholder substitution still works: the {{SCRIPTURE:...}} reference is emitted and substituted with your chosen translation. But the model’s own commentary can pick up KJV-style phrasing or paraphrase verse wording in prose. This depends on the model: with an earlier auto-routed setup, commentary sometimes included lines such as "...that whosoever believeth in him should not perish, but have everlasting life", while repeated test runs with gloo-openai-gpt-6-luna didn’t show it. Solution: Leave tradition unset, which is the recipe’s default. If you need theological framing, add it directly to your system prompt instead; see Adding Theological Framing. If you use tradition, treat the commentary (not just the placeholders) as untrusted, and consider the quotation-detection strategies in Strengthen This Pattern.

Sources

  1. Gao, S., et al. Efficient Tool Use with Chain-of-Abstraction Reasoning. COLING 2025.
  2. The Apologist Project and Fide AI. Scripture Quotation in Generative AI. The test harness is open source at apologist-project/llm-scripture-fidelity.