Skip to content

Change a prompt or a setting without a deploy

View as Markdown

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.
  • An agent to change, in a pack on a runtime, with Kindgi 0.2's CLI. This guide uses the reply drafter from Give an agent its input. Under kindgi dev, blocks come only from the pack's files, as Keep them in the pack shows. The publish commands on this page are for a runtime that isn't kindgi dev, such as one you deploy to: kindgi dev refuses them.
  • The project's id: kindgi projects get-default prints the default project, with its id.
  • For the app's half: Node 22.12 or later, Python 3.11 or later, or Java 17 or later, with Kindgi 0.2's client, and KINDGI_API_URL and KINDGI_API_TOKEN set.

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" }
]
}
}
Excerpt

Publish it, and the settings block:

Terminal window
pnpm exec kindgi blocks publish --project=<project-id> @reply-prompt.json
pnpm exec kindgi 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.json reads the whole block from the file.
  • acme.reply-style and --block-version are the settings block's id and version.
  • --settings is its values, as JSON.
  • --schema=@reply-style.schema.json reads 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.

A pack can bring its blocks instead: one JSON file per block in its blocks/ folder, in the same shape as reply-prompt.json. kindgi deploy publishes them into the project it deploys to, before the agents that use them, so a block and its agent ship together. Ask your coding agent: "Keep my agent's blocks in the pack."

blocks/
reply-prompt.json
reply-prompt.liquid
reply-style.json
  • A settings block's file holds its values, and its schema when it has one, under content.
  • A long template can have its own file, named by templateFile in place of template: "templateFile": "reply-prompt.liquid". The path is from the block's file, and stays inside the blocks folder. The build reads the file into the block. Spell it as the file on disk: a Linux build tells Reply.liquid and reply.liquid apart.
  • kindgi build checks each block, its template and its values against its schema, and refuses a pack whose blocks don't hold up, naming each file. kindgi dev shows the same problems as you edit, and its runtime reads blocks from these files only. When a Python, Java or Scala pack's kindgi package or com.kindgi:kindgi-pack is from before blocks in a pack, and so reads none, kindgi build refuses the pack and kindgi dev says so, each naming the release to upgrade to.
  • The folder is the discovery.blocks pattern in kindgi.config.ts (kindgi.config.json in a Java or Scala pack; blocks under [tool.kindgi.discovery] in a Python pack's pyproject.toml), blocks/**/*.json unless you change it.

A version never changes. When a deploy brings a version the runtime already holds:

  • With the same content, it keeps it, so a deploy whose blocks didn't change publishes none.

  • With other content, the deploy is refused: give the block a new version in its file.

  • Unregistered, by kindgi blocks unregister or by a deploy that failed after publishing it, the deploy is refused, naming the command that reinstates each such block:

    kindgi blocks reinstate acme.reply-prompt --block-version=1.0.0

An agent pins the pack's own version of each block the pack brings, as the pack's files are the agent: a newer version published on the runtime since isn't picked, even one in the agent's range. If the agent's range doesn't take the pack's version, the deploy is refused, naming both. A block the pack doesn't bring pins the newest published version in the agent's range, as before. A runtime from an earlier release doesn't publish a pack's blocks: kindgi deploy checks first, and refuses, naming the release it needs.

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:

agents/reply-drafter/index.ts
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;

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:

tools/refund-check/index.ts
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;

A run passes the prompt's parameters as it does an agent's own:

Terminal window
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"}}'

--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", … }
]
Excerpt

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 version in the pack's file, so a change you save is used at the next run. A block that isn't in the pack, or whose version the agent's range doesn't take, fails the run, naming the block and saying to add it under the pack's blocks folder. A block published to the runtime some other way isn't used: move it into blocks/.

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",
…
}
Excerpt

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, "thinking": "low" }. A temperature goes from 0 to 2. thinking is how much the model thinks: lowest, minimal, low, medium, high, xhigh or max. Its model calls use them, and the version pins the block with its other settings. The block can also name the model the turn prefers, model and providerId, in place of the agent's preferredModel and preferredProvider (never both).

  • A reply that reaches maxOutputTokens stops 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-unsupported warning, as Temperature and thinking describes.
  • A model that thinks spends part of maxOutputTokens on thinking. A level it doesn't take goes as the next one up that it takes, or its highest, with a thinking-level-adjusted warning. Without thinking, its calls get the model's registered default, or else the vendor's, as Temperature and thinking describes.

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:

Terminal window
pnpm exec kindgi 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.

Each version records who published it, publishedBy, and where it came from, source: { "kind": "api" } for a publish like this one, a deployment's id for a version a deploy brought, and a suggested change's id for one that came from a suggested change. GET /v1/blocks/<block-id>/versions/<version> answers both, and the audit log records the publish with you as its actor. A version published before this was recorded has neither.

A new version of a block that an agent version uses as its model settings must be model settings, and can't name the model that agent version names. Otherwise the publish is refused, naming the agent 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:

publish-block.ts
import { createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
const 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));

It prints the block's versions, newest first:

[ '1.1.0', '1.0.0' ]

Under kindgi dev, change the block's file instead: its version and its values. kindgi dev reloads it, the next run reads 1.1.0, and its tool gets { "limit": 100, "needsApproval": false }. A publish there is refused: Error [registry-read-only]: Under kindgi dev, the pack is the source of data blocks: change the block in the pack's files and kindgi dev reloads it.

A new block version doesn't change a published agent version: it keeps running the versions it pins. An agent version that runs new content comes one of two ways:

  • A suggested change, for one project: it takes the block's new values or instructions, publishes the block version, makes the agent version that pins it, and tries both agent versions on the project's test cases. It needs no code change and no deploy. Suggest a change, and test it says how.
  • A deploy: reference the new block version from the agent in the pack, and kindgi deploy.

A developer publishing from code a number a suggested change's version took is 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.

An agent version that pins another settings block version than the one it came from runs read-only tools such as acme-ops.refund-check live on the test cases, rather than reusing the recorded results. So the new refund limit shows in the answers. Those cases count as diverged, which a release gate with replay and maxDiverged: 0 refuses.

A project's Prompts & settings section lists its blocks. A block's page shows its template or values, its versions with Retire, and the agents using it.

  1. 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.
  2. 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 prompt or settings versions ("Refund limit 100")".

Under kindgi dev, a block's page offers no edit and no derive: change the block's file instead, with a new version. Or suggest a change, yourself with Suggest a change or with an Improve run: pinning it writes it into the file (Under kindgi dev).