Your first agent generation
By the end of this tutorial you will have an agent that answers — created, run once, and read back step by step.
Three steps:
- Create an agent on the provider you already have.
- Run a generation and poll it to completion.
- Read the run back — the transcript of what the agent actually did.
Prerequisites
- A
nat_sk_…API key exported asNATURALI_TOKEN. - A project id exported as
PROJECTand a working AI provider id exported asPROVIDER— from either Enable naturali models (no credential of your own) or Create a provider (bring your own key). - A client set up as in that tutorial's Prerequisites.
export NATURALI_TOKEN=nat_sk_...
export PROJECT=proj_V1StGXR8Z5jdHi6B
export PROVIDER=aip_V1StGXR8Z5jdHi6B
1. Create an agent
An agent is a configuration: where the model comes from, what to tell it, and how far it may go. Everything except the model source is optional.
- CLI
- SDK
- curl
naturali create-agent \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER" \
--name refund-explainer \
--instructions 'Answer in one sentence. If you are unsure, say so.'
const { data: agent } = await naturali.agents.createAgent({
path: { project_id: process.env.PROJECT! },
body: {
ai_provider_id: process.env.PROVIDER!,
name: 'refund-explainer',
instructions: 'Answer in one sentence. If you are unsure, say so.',
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/agents" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"ai_provider_id\": \"$PROVIDER\",
\"name\": \"refund-explainer\",
\"instructions\": \"Answer in one sentence. If you are unsure, say so.\"
}"
{
"id": "agent_wq7H4Ka1eDFVsBXM",
"project_id": "proj_cT9LACJi0WypPf5U",
"ai_provider_id": "aip_NzjzJDzoId8cm1Hu",
"name": "refund-explainer",
"model": null,
"instructions": "Answer in one sentence. If you are unsure, say so.",
"max_steps": 20,
"version": 1
}
version is 1 because creating the agent archived its configuration. Every
later write that changes the config archives another one — which is what makes
staged rollouts possible later.
model is null, so the agent generates on the provider's default_model.
export AGENT=agent_wq7H4Ka1eDFVsBXM
2. Run a generation
Generation is background by default: the call returns immediately with a generation id, and the work continues.
- CLI
- SDK
- curl
naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--messages '[{"role":"user","content":"What is our refund window?"}]'
const { data: accepted } = await naturali.agents.createAgentGeneration({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
body: {
messages: [{ role: 'user', content: 'What is our refund window?' }],
},
});
curl -X POST \
"https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/generate" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "What is our refund window?" }] }'
The call answers 202 Accepted:
{
"status": "accepted",
"generation_id": "gen_GvfSi82dWIHZ2QuJ",
"trace_id": "trace_YWhw2oowz6wCVN7N"
}
export GENERATION=gen_GvfSi82dWIHZ2QuJ
Poll it until the status leaves in_progress:
- CLI
- SDK
- curl
naturali get-generation \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
const { data: generation } = await naturali.generations.getGeneration({
path: {
project_id: process.env.PROJECT!,
generation_id: process.env.GENERATION!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations/$GENERATION" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"id": "gen_GvfSi82dWIHZ2QuJ",
"agent_id": "agent_wq7H4Ka1eDFVsBXM",
"status": "completed",
"stop_reason": "stop",
"error": null,
"agent_version": 1,
"usage": {
"cost_usd": 0.00000738,
"input_tokens": 19,
"output_tokens": 26
}
}
A failed status carries a structured error with at least a message — the
usual causes are a credential the vendor rejected and
a model the credential cannot reach.
3. Read the run back
The transcript is the run as a sequence of steps — what the model was asked, what it called, and how it ended.
- CLI
- SDK
- curl
naturali get-generation-transcript \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
const { data: transcript } =
await naturali.generations.getGenerationTranscript({
path: {
project_id: process.env.PROJECT!,
generation_id: process.env.GENERATION!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations/$GENERATION/transcript" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"generation_id": "gen_GvfSi82dWIHZ2QuJ",
"status": "completed",
"step_count": 1,
"input": [{ "role": "user", "content": "What is our refund window?" }],
"steps": [
{
"index": 0,
"text": "I am unsure of our refund window; this information may vary depending on the specific policies of the company or service in question.",
"finish_reason": "stop",
"tool_calls": [],
"tool_results": [],
"usage": { "input_tokens": 19, "output_tokens": 26 }
}
],
"output": {
"content": "I am unsure of our refund window; this information may vary depending on the specific policies of the company or service in question.",
"finish_reason": "stop"
}
}
The agent knows nothing about your policy yet, and its instructions tell it to say so rather than guess.
One step with no tool calls is what a plain question looks like. Bind a tool to the agent and the same transcript grows a step per call, with the arguments the model chose and what came back — which is how you answer "why did it say that" without guessing.
What's next
- Answer from your documents — give this agent the refund policy it just said it does not know.
- Structured output — make the agent return a typed object instead of prose.
- Run tools in your own code — bind a tool and watch the transcript grow a step per call.
- Sessions — keep a conversation going across many turns instead of one-shot runs.
- Run a zero-retention agent — the same run, with nothing of its content recorded.