Concept · 7 of 7

Prompt

A prompt is the string the model actually sees. Most of the time it needs values filled in — a user's name, a set of retrieved documents, the result of a prior step. That filling-in is hydration, and in weave it's a scroll-native event, the same shape as a tool dispatch.

The substrate, again

Prompts look like strings, but that's an authoring view. In weave, a prompt is a question asked on the scroll. When the workflow reaches a step that needs to talk to the model, it emits a prompt.hydrate.request event naming the prompt and whatever inputs it has. A subscriber resolves the placeholders and commits prompt.hydrate.result. Only then does the runtime emit ai.request — always with a fully resolved prompt string.

That's the same pattern as Tool. A tool is a question about what to do. A prompt is a question about what to say. Same event boundary, same subscriber-produces-the-answer model, same replay semantics. The SDK shapes mirror each other because the substrate is the same.

Three tiers

Prompts sit on a spectrum from fully-static to IO-requiring. Which tier you pick is an authoring choice — the runtime mechanics are uniform underneath.

Static

Template with zero placeholders.

A string like "tell me a joke." No inputs, no hydration, no round-trip. The degenerate case — and the common one for simple agents.

0 hydration events. ai.request carries the string as written.

Bound

Template + typed input struct.

A template like "Greet {{.name}}" paired with an input schema. The default SDK subscriber resolves placeholders from the invocation context — no IO, but the hydration events still land on the scroll so replay is uniform.

1 hydration event pair. ai.request follows.

Dynamic

Template + custom hydrator.

A template whose placeholders need IO — vector search, database lookup, session fetch. You register a hydrator the same way you'd register a tool, and it produces the hydrated values as an external commit.

1 hydration event pair. Hydrator did IO. ai.request follows.

Anatomy

A prompt is a named object, same as a tool. You define it once and register it on whatever agents need it.

Static — the degenerate case

var tellJoke = weave.Prompt("joke",
    weave.WithTemplate("Tell me a joke."),
)

Bound — template + typed input

type GreetInput struct {
    Name string `json:"name"`
}

var greet = weave.Prompt("greet",
    weave.WithTemplate("Greet the traveler named {{.name}}."),
    weave.WithInput(GreetInput{}),
)

The WithInput struct names the placeholders. Invoking the agent passes an instance of GreetInput; the default SDK hydrator resolves {{.name}} from that struct.

Dynamic — template + custom hydrator

type SearchInput struct {
    Query string `json:"query"`
}

var search = weave.Prompt("search",
    weave.WithTemplate("Given context:\n{{.docs}}\n\nAnswer the question: {{.query}}"),
    weave.WithInput(SearchInput{}),
    weave.WithHydrator(func(w *weave.HydrateResponse, req *weave.HydrateRequest) {
        var input SearchInput
        if err := json.NewDecoder(req.Body).Decode(&input); err != nil {
            w.Error(err)
            return
        }
        docs, err := vectordb.Search(req.Context(), input.Query)
        if err != nil {
            w.Error(err)
            return
        }
        _ = json.NewEncoder(w).Encode(map[string]any{
            "docs":  formatDocs(docs),
            "query": input.Query,
        })
    }),
)

The hydrator is a handler, just like a tool handler. It receives the typed input, does whatever IO it needs to, and writes the hydrated values out. The result lands on the scroll as an external commit — that's what makes the dynamic case replayable.

Using a prompt in an agent

func main() {
    agent := weave.Agent("librarian",
        weave.WithSystem("You answer questions using retrieved documents."),
        weave.WithPrompt(search),
        weave.WithOutput(Answer{}),
    )

    weave.NewApp(agent).Run()
}

weave.WithPrompt(prompt) takes the prompt object — exactly how tools are passed. The agent wires the prompt into whichever workflow state needs it.

What it looks like on the scroll

Static prompts produce a minimal trace — the prompt is committed inline with the AI request:

# Static prompt — zero hydration events.
[0]  workflow.step            derived      { id: "ask" }
[1]  ai.request               derived      { prompt: "Tell me a joke." }
[2]  ai.response              external     { text: "Why did the …" }

Dynamic prompts insert a hydration round-trip before the AI call. The resolved prompt shows up on ai.request exactly as it went to the model:

# Dynamic prompt — one hydration round-trip before ai.request.
[0]  workflow.step            derived      { id: "ask" }
[1]  prompt.hydrate.request   derived      { prompt: "search", input: { query: "the fall of Castle Ravenfell" } }
[2]  prompt.hydrate.result    external     { docs: "…retrieved lore…", query: "the fall of Castle Ravenfell" }
[3]  ai.request               derived      { prompt: "Given context:\n…\n\nAnswer: the fall of Castle Ravenfell" }
[4]  ai.response              external     { text: "" }

ai.request is always resolved. It never carries a template or a reference — the template lives in the workflow definition, the hydration results live on the scroll, and the ai.request is the derived fact of "this exact string went to the model." That uniformity is why replay works: one event shape, always.

Why this matters for server-side workflows

When you push a workflow to a weave server, the server doesn't know where {{.name}} comes from. It shouldn't. The user's session, your vector store, the current DB connection — none of those belong to weave. What weave provides is the coordination: a scroll event that says "I need this hydrated," and a way for whichever subscriber has the data to respond.

The SDK's default hydrator covers the common case (pull values from the invocation input). Your custom hydrators cover the rest. Either way the server stays provider-and-data agnostic; the meaning lives where the subscribers do.

Status

The prompt concept is the newest primitive in weave and the most visibly "docs-as-spec." Inline {{key}} substitution works today via WithPrompt(string), but the named, typed, hydrator-driven prompt object described on this page is the target — most pills below are designed.

weave.Prompt object
designed
First-class prompt definition symmetric to weave.Tool. Today, prompts are inline strings passed to WithPrompt — the named, composable Prompt object is not yet in the SDK.
Typed input (WithInput)
designed
Binds a template to a struct whose fields name the placeholders. Gives LSP completion, build-time validation, and type-safe invocation.
Static and bound templates
sdk-shimmed
Inline strings with {{key}} substitution from a Context map work today via WithPrompt(string) in pkg/weave/weave.go. The Prompt object wrapper and typed input lands with the SDK redesign.
Custom hydrator (WithHydrator)
designed
Handler registered against the prompt that receives the input and writes hydrated values. Shape mirrors tool handlers — same round-trip, same replay guarantees.
prompt.hydrate.* scroll events
designed
request/result event pair on the scroll, externally commited result. This is how dynamic hydration becomes replayable.
Replay of hydrated values
designed
Recorded hydration results replay as-is. No re-fetching, no re-embedding, no external call on the second run. Lands with the prompt SDK surface.

Not to be confused with

  • LangChain PromptTemplate. Similar authoring shape (template + input variables), but LangChain prompts are in-memory objects scoped to an agent instance. Weave prompts are scroll-native — the hydration is an event, not a method call — which is what makes them portable and replayable.
  • Jinja2 / Handlebars / any template engine. Template syntax is a surface detail. The load-bearing part is the event boundary between "the workflow needs this prompt hydrated" and "here's the hydrated prompt." A template engine handles only the former.
  • A RAG pipeline. Retrieval-augmented generation is one use of dynamic hydration — the hydrator does a vector search — but it is not the only one. Session lookup, recent tool results, user preferences, API calls all fit the same shape. RAG is a pattern, not a separate primitive.