AI quickstart
Make your first model call crash-safe: add a durable AI step, point it at your provider, and watch its spend, tokens, and latency land in the console.
Turn a model call into a durable step, trigger it, and watch its spend, tokens, and latency roll up in the console's AI view.
This picks up where the Quickstart leaves off - it assumes you already have a project, an API key, and a connected runner. If you don't, start there first.
1. Add an AI step
ctx.step.ai.generate makes a model call a durable step: its
result is recorded once under the step id, so a retry after a crash returns the saved result instead
of calling - and paying for - the model again.
import { defineWorkflow } from "@duraton/sdk";
export const triageTicket = defineWorkflow<{ subject: string }>({
name: "ticket.created",
handler: async (ctx) => {
const { text } = await ctx.step.ai.generate("classify", {
model: "claude-opus-4-8",
prompt: `Classify this ticket: ${ctx.event.data.subject}`,
});
return { text };
},
});from duraton import GenerateOptions, define_workflow
from duraton.context import StepContext
async def triage(ctx: StepContext) -> object:
result = await ctx.step.ai.generate("classify", GenerateOptions(
model="claude-opus-4-8",
prompt=f"Classify this ticket: {ctx.event.data['subject']}",
))
return {"text": result.text}
triage_ticket = define_workflow("ticket.created", triage)type Ticket struct {
Subject string `json:"subject"`
}
var triageTicket = duraton.DefineWorkflow(duraton.WorkflowDefinition{
Name: "ticket.created",
Handler: func(c *duraton.Context) (any, error) {
ticket, err := duraton.EventData[Ticket](c)
if err != nil {
return nil, err
}
res, err := duraton.Generate(c, "classify", duraton.GenerateOptions{
Model: "claude-opus-4-8",
Prompt: "Classify this ticket: " + ticket.Subject,
})
if err != nil {
return nil, err
}
return map[string]string{"text": res.Text}, nil
},
})2. Set your provider key
The built-in provider is Anthropic. Your runner makes the model call, so the key stays with your runner - Duraton meters tokens but never sees your key, your prompt, or the response. Set it in the runner's environment:
export ANTHROPIC_API_KEY="sk-ant-..."Duraton is bring-your-own-keys. Omit apiKey on the call and the provider SDK reads its
conventional env var (ANTHROPIC_API_KEY); pass apiKey per call to override it. Either way the
key is used for that one call and is never recorded in the run history.
3. Trigger it and watch spend land
Send the event your workflow listens for - with the SDK client, or over the REST API:
import { createClient } from "@duraton/sdk";
const duraton = createClient({
url: process.env.DURATON_URL!,
apiKey: process.env.DURATON_API_KEY,
});
await duraton.events.send({
name: "ticket.created",
data: { subject: "Refund not received" },
});import os
from duraton.client import AsyncDuratonClient, SendEventInput
async with AsyncDuratonClient(
url=os.environ["DURATON_URL"],
api_key=os.environ.get("DURATON_API_KEY"),
) as dx:
await dx.events.send(SendEventInput(
name="ticket.created",
data={"subject": "Refund not received"},
))import (
"context"
"encoding/json"
"duraton.dev/sdk-go/client"
)
dx := client.New(client.Options{})
_, err := dx.Events.Send(context.Background(), client.SendEventInput{
Name: "ticket.created",
Data: json.RawMessage(`{"subject":"Refund not received"}`),
})curl -X POST "$DURATON_URL/events" \
-H "Authorization: Bearer $DURATON_API_KEY" \
-d '{"name":"ticket.created","data":{"subject":"Refund not received"}}'As the AI step runs, open the AI view in the console. Once the first call is recorded it fills in: total spend and tokens for the window, average latency, cache hit rate, and breakdowns of cost by hour, by model, and by workflow.
Read it over MCP
An AI agent reading your project sees the same rollup: Duraton exposes the AI spend summary as the
ai_spend MCP tool, so an assistant can pull window totals and the by-model /
by-workflow breakdowns without the console.