Change a prompt or a setting without a deploy
This guide moves an agent's prompt and settings into data blocks, so the people who know the work change them and make a new agent version, with no code change and no deploy.
A data block is a prompt, or a set of settings, kept in the runtime instead of in your code. Each change is a new version of the block, and an agent version pins the block versions it uses. So the people who know the work can change a prompt or a limit and make a new agent version from it: no code change, and no deploy.
There are two kinds:
- A prompt block is an agent's instructions: a Liquid template, with the parameters it takes, rendered as the agent's instructions are.
- A settings block is a JSON object of values, with an optional JSON Schema they must satisfy. The agent's instructions and its tools read them.
Before you start
Section titled “Before you start”- An agent to change, in a pack on a runtime such as
kindgi dev, with Kindgi 0.1's CLI. This guide uses the reply drafter from Give an agent its input. - The project's id:
kindgi projects get-defaultprints the Default project, with itsid. - For the app's half: Node 22.12 or later, Python 3.11 or later, or
Java 17 or later, with Kindgi 0.1's client, and
KINDGI_API_URLandKINDGI_API_TOKENset.
Publish the blocks
Section titled “Publish the blocks”The examples on this page move the reply drafter's instructions from
Give an agent its input into a prompt
block, acme.reply-prompt, and add a settings block, acme.reply-style:
how long a reply may be, and up to what amount a refund needs no approval.
A prompt block with parameters is published from a JSON file. Ask your coding agent to write the prompt block:
I want my agent's instructions in a prompt block, with the tone as a parameter.For reference, the block it writes looks like this. This guide saves it as
reply-prompt.json:
{ "id": "acme.reply-prompt", "version": "1.0.0", "kind": "prompt", "description": "The reply drafter's instructions", "content": { "template": "You draft short replies to Acme customers, in a {{ tone }} tone, in at most {{ settings[\"acme.reply-style\"].maxSentences }} sentences.\nWhen a customer asks for money back, check the amount with the refund check tool, and say whether a person needs to approve the refund.\nSign every reply as {{ signature }}.", "parameters": [ { "name": "tone", "type": "string", "description": "friendly, formal, …" }, { "name": "signature", "type": "string", "default": "The Acme team" } ] }}Publish it, and the settings block:
pnpm exec kindgi blocks publish --project=<project-id> @reply-prompt.jsonpnpm exec kindgi blocks publish acme.reply-style --block-version=1.0.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":50}' --schema=@reply-style.schema.jsonuv run kindgi blocks publish --project=<project-id> @reply-prompt.jsonuv run kindgi blocks publish acme.reply-style --block-version=1.0.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":50}' --schema=@reply-style.schema.json./kindgiw blocks publish --project=<project-id> @reply-prompt.json./kindgiw blocks publish acme.reply-style --block-version=1.0.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":50}' --schema=@reply-style.schema.json./kindgiw blocks publish --project=<project-id> @reply-prompt.json./kindgiw blocks publish acme.reply-style --block-version=1.0.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":50}' --schema=@reply-style.schema.json--project=<project-id>is the project the block lives in, where<project-id>is its id.@reply-prompt.jsonreads the whole block from the file.acme.reply-styleand--block-versionare the settings block's id and version.--settingsis its values, as JSON.--schema=@reply-style.schema.jsonreads the JSON Schema its values must satisfy from a file.
Each prints the block's id and version. The second prints:
{ "blockId": "acme.reply-style", "version": "1.0.0"}-
The template reads a settings block as
{{ settings["acme.reply-style"].maxSentences }}: the block's id in quotes, then the key. -
A settings block's schema checks its values, and later versions' too. A version published without one keeps the latest version's. A version that gives one replaces it, and
--schema='{}'drops the check on purpose. Values that don't fit are refused:Block "acme.reply-style" can't take this version✗ /content/values/refundLimit: must be >= 0 (the schema of version 1.0.0)
kindgi blocks list, show and versions read them. unregister and
reinstate retire a version and bring it back.
Use them in an agent
Section titled “Use them in an agent”The agent names its prompt block and its settings blocks by range, as it names its tools. Ask your coding agent: "Have my agent use its prompt block and settings block." For reference, the agent looks like this:
import { defineAgent } from '@kindgi/sdk/define';import type { AgentId, Semver } from '@kindgi/sdk/types';
const defined = defineAgent({ id: 'acme-ops.reply-drafter' as AgentId, version: '1.0.0' as Semver, name: 'Reply drafter', instructions: { prompt: 'acme.reply-prompt', version: '^1.0.0' }, settings: [{ id: 'acme.reply-style', version: '^1.0.0' }], capabilities: [{ needs: [{ feature: 'tool-use' as const }] }], tools: [{ id: 'acme-ops.refund-check', version: '^0.1.0' }], retrieval: [], guardrails: [],});
if (defined.kind === 'err') throw new Error(defined.error.message);export default defined.value;from kindgi import Agent
from ..tools.refund_check import refund_check
reply_drafter = Agent( id="acme-ops.reply-drafter", version="1.0.0", name="Reply drafter", instructions={"prompt": "acme.reply-prompt", "version": "^1.0.0"}, settings=[{"id": "acme.reply-style", "version": "^1.0.0"}], capabilities=[{"needs": [{"feature": "tool-use"}]}], tools=[refund_check],)package acmeops.agents;
import acmeops.tools.RefundCheck;import com.kindgi.pack.Agent;import java.util.List;import java.util.Map;
public final class ReplyDrafter { public static final Agent AGENT = Agent.define("acme-ops.reply-drafter") .version("1.0.0") .name("Reply drafter") .instructions(Map.of("prompt", "acme.reply-prompt", "version", "^1.0.0")) .set("settings", List.of(Map.of("id", "acme.reply-style", "version", "^1.0.0"))) .capability(Map.of("needs", List.of(Map.of("feature", "tool-use")))) .tool(RefundCheck.TOOL) .build();
private ReplyDrafter() {}}package acmeops.agents
import acmeops.tools.RefundCheckimport com.kindgi.pack.scaladsl._
object ReplyDrafter { val agent: Agent = Agent("acme-ops.reply-drafter") .version("1.0.0") .name("Reply drafter") .instructions(Map("prompt" -> "acme.reply-prompt", "version" -> "^1.0.0")) .set("settings", Seq(Map("id" -> "acme.reply-style", "version" -> "^1.0.0"))) .capability(Map("needs" -> Seq(Map("feature" -> "tool-use")))) .tool(RefundCheck.tool) .build()}The prompt block brings its parameters, so the agent declares none. A tool
reads the settings blocks of the agent version that calls it, as
ctx.settings. Ask your coding agent: "I want a tool that says whether a
refund needs a person's approval, with the limit from my settings block." For
reference, the tool looks like this:
import { defineTool } from '@kindgi/sdk/define';import type { ToolId } from '@kindgi/sdk/types';import { z } from 'zod';
const defined = defineTool({ id: 'acme-ops.refund-check' as ToolId, description: 'Says whether a refund of this amount needs a person to approve it.', version: '0.1.0', input: z.object({ amount: z.number().positive() }), output: z.object({ needsApproval: z.boolean(), limit: z.number() }), effects: [], mutating: false, handler: async (input, ctx) => { // The agent version's settings block: an expert changes the limit, not this code. const limit = Number(ctx.settings?.['acme.reply-style']?.refundLimit ?? 0); return { needsApproval: input.amount > limit, limit }; },});
if (defined.kind === 'err') throw new Error(defined.error.message);export default defined.value;from pydantic import BaseModel, Field
from kindgi import ToolContext, tool
class RefundAmount(BaseModel): amount: float = Field(gt=0)
class RefundCheck(BaseModel): needs_approval: bool = Field(alias="needsApproval") limit: float
@tool(id="acme-ops.refund-check", mutating=False)def refund_check(refund: RefundAmount, ctx: ToolContext) -> RefundCheck: """Says whether a refund of this amount needs a person to approve it.""" # The agent version's settings block: an expert changes the limit, not this code. limit = float(ctx.settings.get("acme.reply-style", {}).get("refundLimit", 0)) return RefundCheck(needsApproval=refund.amount > limit, limit=limit)package acmeops.tools;
import com.kindgi.pack.Tool;import jakarta.validation.constraints.Positive;import java.util.Map;
public final class RefundCheck { public record Input(@Positive double amount) {}
public record Output(boolean needsApproval, double limit) {}
public static final Tool<Input, Output> TOOL = Tool.define("acme-ops.refund-check") .description("Says whether a refund of this amount needs a person to approve it.") .version("0.1.0") .input(Input.class) .output(Output.class) .mutating(false) .handler((input, ctx) -> { // The agent version's settings block: an expert changes the limit, not this code. Map<String, Object> style = ctx.settings().getOrDefault("acme.reply-style", Map.of()); double limit = style.get("refundLimit") instanceof Number n ? n.doubleValue() : 0; return new Output(input.amount() > limit, limit); });
private RefundCheck() {}}ctx.settings() maps each settings block the calling agent version pins, by
id, to its values.
package acmeops.tools
import com.kindgi.pack.scaladsl._import jakarta.validation.constraints.Positive
object RefundCheck { final case class Input(@Positive amount: Double) final case class Output(needsApproval: Boolean, limit: Double)
val tool: Tool[Input, Output] = Tool[Input, Output]("acme-ops.refund-check") .description("Says whether a refund of this amount needs a person to approve it.") .version("0.1.0") .readOnly .handler { (input, ctx) => // The agent version's settings block: an expert changes the limit, not this code. val limit = Option(ctx.settings.get("acme.reply-style")).flatMap(s => Option(s.get("refundLimit"))) match { case Some(n: Number) => n.doubleValue case _ => 0.0 } Output(needsApproval = input.amount > limit, limit = limit) }}ctx.settings maps each settings block the calling agent version pins, by
id, to its values (a Java map).
A run passes the prompt's parameters as it does an agent's own:
pnpm exec kindgi runs start --agent=acme-ops.reply-drafter --input='{"userMessage":"I was charged twice for my $80 order. Can I get my money back?","parameters":{"tone":"friendly"}}'uv run kindgi runs start --agent=acme-ops.reply-drafter --input='{"userMessage":"I was charged twice for my $80 order. Can I get my money back?","parameters":{"tone":"friendly"}}'./kindgiw runs start --agent=acme-ops.reply-drafter --input='{"userMessage":"I was charged twice for my $80 order. Can I get my money back?","parameters":{"tone":"friendly"}}'./kindgiw runs start --agent=acme-ops.reply-drafter --input='{"userMessage":"I was charged twice for my $80 order. Can I get my money back?","parameters":{"tone":"friendly"}}'--input's parameters are the prompt's parameters: here, tone. The run's
appended messages show the tool reading the settings block's limit:
"appended": [ { "role": "user", "content": "I was charged twice for my $80 order. Can I get my money back?", … }, { "role": "agent", "content": { "text": "", "toolCalls": [{ "name": "acme-ops.refund-check", "arguments": { "amount": 80 }, … }] }, … }, { "role": "tool", "content": { "limit": 50, "needsApproval": true }, … }, { "role": "agent", "content": "We're so sorry you were charged twice—that's definitely a mistake on our end! The good news is we can process your $80 refund, but it will need approval from one of our team members, so you should see it within a couple of business days. Thanks for your patience!\n\nThe Acme team", … }]The run's journal, which kindgi runs journal <run-id> prints, has the
versions it used, in its setup step. It has the instructions as rendered,
in render-prompt:
{ "blockVersions": { "prompts": { "acme.reply-prompt": "1.0.0" }, "settings": { "acme.reply-style": "1.0.0" } } }{ "rendered": "You draft short replies to Acme customers, in a friendly tone, in at most 3 sentences.\nWhen a customer asks for money back, check the amount with the refund check tool, and say whether a person needs to approve the refund.\nSign every reply as The Acme team." }Under kindgi dev, each run uses the newest block versions in the
agent's ranges, so a block version you publish is used at the next run.
A published agent version pins them. When it's published, by
kindgi deploy or the API, each range resolves once, like its tools', as
Agent and tool versions describes:
{ "version": "1.0.0", "pins": { "tools": { "acme-ops.refund-check": "0.1.0" }, "prompts": { "acme.reply-prompt": "1.0.0" }, "settings": { "acme.reply-style": "1.0.0" } }, "pinsDigest": "sha256:3a7b0e31e9b6fb9149ea98dc3d0be451512e9d765f1321a834a5a4fad2fffe09", …}A block version that's missing or of the wrong kind refuses the publish,
naming each. So do model settings with values a model can't take:
Agent "acme-ops.reply-drafter" uses tool or data-block versions it can't pin (1 issue).
Model settings are a settings block the agent names as modelSettings,
such as { "temperature": 0.2, "maxOutputTokens": 400 }. A temperature goes
from 0 to 2. Its model calls use them, and the version pins the block with
its other settings.
- A reply that reaches
maxOutputTokensstops there, mid-sentence if need be, and the run still completes. - Some models take no temperature, such as the Claude 5.5 and GPT-6
models. Their calls go without it, and the turn carries a
sampling-unsupportedwarning, as Temperature and thinking describes. - A model that thinks spends part of
maxOutputTokenson thinking.
Change a block
Section titled “Change a block”A change is a new version of the block, so the people who know the work change the values, not the code. With the CLI:
pnpm exec kindgi blocks publish acme.reply-style --block-version=1.1.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":100}'uv run kindgi blocks publish acme.reply-style --block-version=1.1.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":100}'./kindgiw blocks publish acme.reply-style --block-version=1.1.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":100}'./kindgiw blocks publish acme.reply-style --block-version=1.1.0 --project=<project-id> --settings='{"maxSentences":3,"refundLimit":100}'--block-version=1.1.0 is the new version, and --settings its values: a
higher refund limit. It prints the block's id and version.
From your code, such as an admin page of your own, ask your coding agent: "I want to publish a new version of my settings block from my app." For reference, the code looks like this:
import { createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devconst project = await kindgi.projects.getDefault();
// The settings block's next version: a higher refund limit, and no code change.await kindgi.blocks.publish( { id: 'acme.reply-style', version: '1.1.0', kind: 'settings', content: { values: { maxSentences: 3, refundLimit: 100 } }, }, { projectId: project.id },);
const versions = await kindgi.blocks.versions.list('acme.reply-style');console.log(versions.data.map((block) => block.version));from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devproject = kindgi.projects.get_default()
# The settings block's next version: a higher refund limit, and no code change.kindgi.blocks.publish( project_id=project.id, id="acme.reply-style", version="1.1.0", kind="settings", content={"values": {"maxSentences": 3, "refundLimit": 100}},)
versions = kindgi.blocks.versions.list("acme.reply-style")print([block.version for block in versions.data])package com.acme.app;
import com.kindgi.client.Kindgi;import com.kindgi.client.models.Block;import com.kindgi.client.models.BlockKind;import com.kindgi.client.models.Project;import com.kindgi.client.models.PublishBlockBody;import com.kindgi.client.models.SettingsBlockContent;import java.util.Map;import java.util.UUID;
public final class PublishBlock { public static void main(String[] args) { Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev Project project = kindgi.projects().getDefault();
// The settings block's next version: a higher refund limit, and no code change. kindgi.blocks().publish(PublishBlockBody.builder() .projectId(UUID.fromString(project.id())) .id("acme.reply-style") .version("1.1.0") .kind(BlockKind.SETTINGS) .content(SettingsBlockContent.builder().values(Map.of("maxSentences", 3, "refundLimit", 100)).build()) .build());
System.out.println(kindgi.blocks().versions().list("acme.reply-style").data().stream() .map(Block::version).toList()); }}It prints the versions below without quotes: [1.1.0, 1.0.0].
package acme.app
import com.kindgi.client.Kindgiimport com.kindgi.client.models.{BlockKind, PublishBlockBody, SettingsBlockContent}import java.util.UUIDimport scala.jdk.CollectionConverters._
object PublishBlock { def main(args: Array[String]): Unit = { val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev val project = kindgi.projects().getDefault()
// The settings block's next version: a higher refund limit, and no code change. kindgi.blocks().publish(PublishBlockBody.builder() .projectId(UUID.fromString(project.id)) .id("acme.reply-style") .version("1.1.0") .kind(BlockKind.SETTINGS) .content(SettingsBlockContent.builder() .values(Map[String, AnyRef]("maxSentences" -> Int.box(3), "refundLimit" -> Int.box(100)).asJava) .build()) .build())
println(kindgi.blocks().versions().list("acme.reply-style").data.asScala.map(_.version).mkString("[", ", ", "]")) }}It prints the versions below without quotes: [1.1.0, 1.0.0].
It prints the block's versions, newest first:
[ '1.1.0', '1.0.0' ]Under kindgi dev, that's all: the next run reads 1.1.0, and its tool gets
{ "limit": 100, "needsApproval": false }.
Make an agent version that runs it
Section titled “Make an agent version that runs it”A new block version doesn't change a published agent version: it keeps running the versions it pins. To use the new one, derive an agent version: the same agent, with some of its block pins swapped. It needs no code change and no deploy.
pnpm exec kindgi agents derive acme-ops.reply-drafter --from=1.0.0 --setting=acme.reply-style=1.1.0 --label="Refund limit 100"uv run kindgi agents derive acme-ops.reply-drafter --from=1.0.0 --setting=acme.reply-style=1.1.0 --label="Refund limit 100"./kindgiw agents derive acme-ops.reply-drafter --from=1.0.0 --setting=acme.reply-style=1.1.0 --label="Refund limit 100"./kindgiw agents derive acme-ops.reply-drafter --from=1.0.0 --setting=acme.reply-style=1.1.0 --label="Refund limit 100"--from=1.0.0is the version to start from.--setting=acme.reply-style=1.1.0swaps in that settings block version.--labelsays what changed.
It prints the new agent version:
{ "id": "acme-ops.reply-drafter", "version": "1.0.1", … "pins": { "tools": { "acme-ops.refund-check": "0.1.0" }, "prompts": { "acme.reply-prompt": "1.0.0" }, "settings": { "acme.reply-style": "1.1.0" } }, "derivedFrom": { "version": "1.0.0", "reason": "edited", "label": "Refund limit 100", "by": "user:35aa4131-bc65-43c9-80ba-8931a53fafdc" }}From your code, ask your coding agent: "I want a new version of my agent that uses the new refund limit." For reference, the code looks like this:
import { createClient } from '@kindgi/sdk/client';import type { AgentId } from '@kindgi/sdk/types';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN
// Version 1.0.0 with version 1.1.0 of its settings block: no code change, no deploy.const derived = await kindgi.agents.versions.derive('acme-ops.reply-drafter' as AgentId, { from: '1.0.0', pins: { settings: { 'acme.reply-style': '1.1.0' } }, label: 'Refund limit 100',});console.log(derived.version, derived.pins?.settings, derived.derivedFrom);from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN
# Version 1.0.0 with version 1.1.0 of its settings block: no code change, no deploy.derived = kindgi.agents.derive_version( "acme-ops.reply-drafter", from_="1.0.0", pins={"settings": {"acme.reply-style": "1.1.0"}}, label="Refund limit 100",)print(derived.version, derived.pins.settings if derived.pins else None, derived.derived_from)- The new version is the next free patch after the highest one, and
derivedFromsays where it came from. Runs choose it like any other version. The same swap again returns that version, with200, not a new one. - Swap prompts with
--prompt=<block-id>=<version>, orpins.promptsin the clients, and settings with--setting. Repeat either for several. - Only blocks the version already uses can be swapped, and only on a version that has pins.
- Under
kindgi dev, there's nothing to derive: each run already uses the newest block versions, and a derive is refused:Error [registry-read-only]: Under kindgi dev, the pack is the source of agents: change the agent in the pack's files and kindgi dev reloads it. - Then a developer publishing the next number from code gets told which
one is free:
Agent "acme-ops.reply-drafter" version "1.0.1" is already registered, and versions never change; publish it as 1.0.2, the next free version.
Before anyone uses the new version,
compare it on a test set. Because 1.0.1 pins
another settings block version than 1.0.0, the comparison runs read-only
tools such as acme-ops.refund-check live, rather than reusing the recorded
results. So the new refund limit shows in the answers. Those cases count as
diverged, which a gate with replay and maxDiverged: 0 refuses.
In the console
Section titled “In the console”A project's Data blocks section lists its blocks. A block's page shows its template or values, its versions with Retire, and the agents using it.
- Edit as a new version starts from the version you're viewing and proposes the next number, with an optional note for this version. Published versions never change: this publishes a new one.
- After it's published, Derive a version next to an agent using the block makes the agent version that runs it: "Derive a new version of Reply drafter with acme.reply-prompt 1.0.1?"
An agent's page says where its instructions and settings come from, and where a derived version came from: "Derived from 1.0.0 with new data-block versions ("Refund limit 100")".
Under kindgi dev, a block's page says each pack agent "Runs the newest
version in ^1.0.0 on each run (kindgi dev)", and offers no derive.