Skip to main content

What Are Thread API Endpoints?

Thread API endpoints are custom routes that operate on specific conversation threads. They allow you to:
  • Access thread data: Query messages, logs, and metadata
  • Perform operations: Export conversations, generate summaries, update state
  • Extend the API: Add domain-specific functionality beyond built-in routes
Thread API endpoints are file-based and auto-discovered. Create a TypeScript file in agents/api/ and export your endpoint definition—no manual registration needed.

Quick Start

Create your first endpoint in agents/api/status.ts:
import { defineThreadEndpoint } from '@standardagents/spec';

export default defineThreadEndpoint(async (req, state) => {
  const { messages, total } = await state.getMessages();

  return Response.json({
    threadId: state.threadId,
    agentId: state.agentId,
    messageCount: total,
  });
});
Your endpoint is now available at GET /api/threads/:id/status. The framework auto-discovers it from the agents/api/ directory.

File-Based Routing

API files map directly to routes based on their filename:
FileHTTP MethodRoute
status.tsGET/api/threads/:id/status
status.get.tsGET/api/threads/:id/status
export.post.tsPOST/api/threads/:id/export
metadata.put.tsPUT/api/threads/:id/metadata
archive.delete.tsDELETE/api/threads/:id/archive

Method Suffixes

Append the HTTP method to the filename to specify which method the endpoint handles:
  • .get.ts — GET requests (default if no suffix)
  • .post.ts — POST requests
  • .put.ts — PUT requests
  • .delete.ts — DELETE requests
  • .patch.ts — PATCH requests
Files without a method suffix default to GET.

Nested Routes

Create subdirectories for nested route paths:
agents/api/
├── status.ts                    # GET /api/threads/:id/status
├── export.post.ts               # POST /api/threads/:id/export
└── messages/
    └── count.ts                 # GET /api/threads/:id/messages/count

Handler Function

The defineThreadEndpoint function wraps your handler and automatically:
  1. Extracts the thread ID from the URL
  2. Looks up the thread and creates a ThreadState
  3. Handles errors (400 for missing ID, 404 for not found, 500 for exceptions)

Signature

defineThreadEndpoint(
  handler: (req: Request, state: ThreadState) => Response | Promise<Response>
)

Parameters

req — Standard Web API Request object state — ThreadState providing access to thread identity, messages, file system, and more. Note: state.execution is always null in endpoints since the thread is at rest.

ThreadState in Endpoints

The state parameter provides access to the thread’s data:

Identity

Access thread identity directly:
state.threadId     // Thread UUID
state.agentId      // Agent name
state.userId       // Optional user identifier (or null)
state.createdAt    // Creation timestamp

getMessages

Retrieve message history from the thread:
const { messages, total, hasMore } = await state.getMessages({
  limit: 100,         // Max messages
  offset: 0,          // Skip count
  order: 'desc',      // 'asc' or 'desc'
  includeSilent: false, // Include UI-only messages
  maxDepth: 0         // Max nesting depth (0 = top-level only)
});

File System

Access the thread’s file system:
// Read/write files
const data = await state.readFile('/data/config.json');
await state.writeFile('/output/result.json', JSON.stringify(data), 'application/json');

// List and search
const { entries } = await state.readdirFile('/data');
const { paths } = await state.findFiles('**/*.json');
const matches = await state.grepFiles('error');

Resource Loading

Load agent, prompt, or model definitions:
const agent = await state.loadAgent('my-agent');
const prompt = await state.loadPrompt('my-prompt');
const model = await state.loadModel('gpt-4o');

Request Handling

Reading Request Body

export default defineThreadEndpoint(async (req, state) => {
  // JSON
  const body = await req.json();

  // Form data
  const formData = await req.formData();

  // Plain text
  const text = await req.text();

  // ...
});

Query Parameters

export default defineThreadEndpoint(async (req, state) => {
  const url = new URL(req.url);
  const limit = parseInt(url.searchParams.get('limit') || '10');
  const format = url.searchParams.get('format') || 'json';

  const { messages } = await state.getMessages({ limit });
  // ...
});

Headers

export default defineThreadEndpoint(async (req, state) => {
  const authHeader = req.headers.get('Authorization');

  if (!authHeader) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // ...
});

Error Handling

Wrap operations in try/catch for robust error handling:
export default defineThreadEndpoint(async (req, state) => {
  try {
    const { messages } = await state.getMessages();

    // Process messages...

    return Response.json({ success: true, data: result });
  } catch (error) {
    console.error('Endpoint error:', error);

    return Response.json(
      { error: error.message || 'Internal server error' },
      { status: 500 }
    );
  }
});
The framework automatically handles common errors like missing thread IDs (400) and threads not found (404). Your handler only needs to handle domain-specific errors.

Best Practices

API handlers run in the request path. Keep operations quick:
// Good - paginated query
const { messages } = await state.getMessages({ limit: 50 });

// Avoid - loading entire history for large threads
const { messages } = await state.getMessages({ limit: 10000 });
Always wrap risky operations:
try {
  const data = await req.json();
  // Process data...
} catch (error) {
  return Response.json(
    { error: 'Invalid JSON body' },
    { status: 400 }
  );
}
Check request data before processing:
const { format, limit } = await req.json();

if (!['json', 'markdown', 'csv'].includes(format)) {
  return Response.json(
    { error: 'Invalid format. Use: json, markdown, or csv' },
    { status: 400 }
  );
}

if (limit && (limit < 1 || limit > 1000)) {
  return Response.json(
    { error: 'Limit must be between 1 and 1000' },
    { status: 400 }
  );
}
Name files clearly to indicate their purpose:
agents/api/
├── summary.get.ts      # Clear: GET summary
├── export.post.ts      # Clear: POST to export
├── analytics.get.ts    # Clear: GET analytics
└── data.ts             # Unclear: what does this do?

Next Steps