Skip to content
3 min read · 545 words

Guide: Add an Endpoint

Worked example: GET /agents/{id}/tasks/summary — a task-count summary endpoint — built backend-first, then exposed to the browser. Adapt the steps; keep the contracts.

Step 1 — Define the response schema

python
class TaskSummaryResponse(BaseModel):
    agent_id: int
    total: int
    open: int
    in_progress: int
    completed: int

Request bodies (for POST/PUT) get an XRequest schema the same way. Conventions: Schemas.

Step 2 — Write the handler with the house pattern

python
@router.get("/{agent_id}/tasks/summary", response_model=schemas.TaskSummaryResponse)
def get_task_summary(agent_id: int, db: Session = Depends(get_db)):
    agent = db.query(models.Agent).filter(models.Agent.id == agent_id).first()
    if agent is None:
        raise HTTPException(status_code=404, detail="Agent not found")     # guard first
 
    tasks = db.query(models.Task).filter(models.Task.agent_id == agent_id).all()
    return {
        "agent_id": agent_id,
        "total": len(tasks),
        "open": len([t for t in tasks if t.status == "open"]),
        "in_progress": len([t for t in tasks if t.status == "in_progress"]),
        "completed": len([t for t in tasks if t.status == "completed"]),
    }

Checklist by endpoint kind:

If your endpoint…Then also…
executes a tool/LLMgate with agent_has_tool (403) and log_tool_call success and failure paths (contract)
calls an LLMput the call in a service, time it, attach cost via estimate_openai_cost (conventions)
is a multi-stage pipelinecreate_workflow_run first, log_workflow_step per stage (engine)
writes rowsone db.commit() at the end; db.refresh() anything whose generated fields you return

Step 3 — Verify against the live server

Hot reload picks the change up; exercise it via Swagger UI (http://localhost:8001/docs) or:

bash
curl http://localhost:8001/agents/1/tasks/summary
curl http://localhost:8001/agents/999/tasks/summary   # → 404, confirm the guard

Step 4 — Expose to the browser (only if the UI needs it)

Server components can fetch the backend directly (reads pattern). For client-side calls, add the proxy route:

ts
import { NextResponse } from "next/server";
 
const API_BASE_URL = process.env.AGENTOPS_API_BASE_URL ?? "http://localhost:8001";
 
export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;                       // Next 15: params is a Promise
  try {
    const response = await fetch(`${API_BASE_URL}/agents/${id}/tasks/summary`, {
      cache: "no-store",
    });
    const text = await response.text();
    let data: unknown = null;
    try { data = text ? JSON.parse(text) : null; }
    catch { data = { detail: text || "Unexpected backend response" }; }
    return NextResponse.json(data, { status: response.status });
  } catch {
    return NextResponse.json(
      { detail: `Unable to reach ${API_BASE_URL}/agents/${id}/tasks/summary` },
      { status: 502 },
    );
  }
}

Same skeleton as the existing proxy — defensive parse, exact status relay, 502 on unreachable (API Proxy). Mirror the response type in the consuming component and run npm run typecheck.

Step 5 — Document it

Add the route to the API Reference section it belongs to, and update the relevant module page (Routers: Agents). Docs build must stay warning-free.