Start
One-off agent
Use this path for a request that does not need a conversation to survive a disconnect. It is useful for a CLI command, a form submission, a build step, or a small background task.
The one-off path still gives you typed prompt inputs, model selection, thinking level, tool declarations, limits, provider normalization, and Zod validation. Add a durable thread when the caller needs reconnectable streaming or multi-turn state.
Create the prompt
---
name: answer-support-question
description: Answer one support question from approved product facts.
model: inherit
thinking: medium
tools:
- docs.search
input:
customer.name: string
question: string
limits:
steps: 8
tools: 12
---
You are a support agent.
Use docs.search for product facts. Do not invent policy.
Answer {{customer.name}} clearly and briefly.
Question:
{{question}}
The file path becomes its stable slug:
prompts/support/answer.prompt.md → support/answer
Keep this file in Git. A changed prompt creates a new source hash when you compile it or publish it through a host control plane.
Compile and call an adapter
Flary’s provider adapters use one normalized request shape. The adapter can complete a request or return normalized stream events.
import { readFile } from "node:fs/promises";
import { compilePrompt } from "flary/prompts";
import {
OpenAICompatibleAdapter,
ProviderAdapterRegistry,
} from "flary/providers";
const source = {
path: "prompts/support/answer.prompt.md",
content: await readFile(
"prompts/support/answer.prompt.md",
"utf8",
),
};
const prompt = await compilePrompt(source, {
callerModel: "openai/gpt-5.6",
values: {
customer: { name: "Ada" },
question: "How do I change my plan?",
},
});
if (!prompt.resolvedModel) {
throw new Error("Select a model with callerModel or prompt front matter");
}
const providers = new ProviderAdapterRegistry({
adapters: [
new OpenAICompatibleAdapter({
id: "openai",
baseUrl: "https://api.openai.com/v1",
apiKey: process.env.OPENAI_API_KEY,
}),
],
});
const adapter = providers.resolve(prompt.resolvedModel.provider);
const response = await adapter.complete({
model: prompt.resolvedModel.model,
messages: [{ role: "user", content: prompt.rendered }],
reasoningEffort: prompt.thinking === "inherit" ? undefined : prompt.thinking,
});
console.log(response.content);
The adapter never receives a prompt file path or a secret-like input value. Your application resolves credentials in server code and passes them to the adapter constructor or a Flary provider resolver.
Stream a one-off call
for await (const event of adapter.stream({
model: prompt.resolvedModel.model,
messages: [{ role: "user", content: prompt.rendered }],
})) {
if (event.type === "text_delta") process.stdout.write(event.delta);
if (event.type === "finish") console.log("\n", event.response.usage);
if (event.type === "error") console.error(event.error);
}
Use the durable run API when a stream must resume after the browser, request, or Worker isolate ends. A one-off stream cannot restore an interrupted tool write.
Add Zod output validation
The normalized provider request supports JSON output. Validate the result in the application boundary:
import { z } from "zod";
const answer = z.object({
message: z.string(),
citations: z.array(z.string()),
});
const response = await adapter.complete({
model: prompt.resolvedModel.model,
messages: [{ role: "user", content: prompt.rendered }],
responseFormat: {
type: "json_object",
schema: z.toJSONSchema(answer),
},
});
const value = answer.parse(JSON.parse(response.content));
When to move to a thread
Use a durable thread if any of these are true:
- The user can leave and return while work continues.
- The agent needs more than one turn.
- A tool can wait for approval or user input.
- The agent edits a workspace or runs a build.
- The product needs event replay, usage records, or Recall.
Continue with Durable threads and Self-hosting.