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/builder';

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

  return Response.json({
    threadId: metadata.id,
    agentId: metadata.agent_id,
    messageCount: messages.length,
  });
});
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 thread metadata
  3. Gets the DurableObject instance
  4. Handles errors (400 for missing ID, 404 for not found, 500 for exceptions)

Signature

defineThreadEndpoint(
  handler: (
    req: Request,
    context: { instance: ThreadInstance; metadata: ThreadMetadata }
  ) => Response | Promise<Response>
)

Parameters

req — Standard Web API Request object context.instance — The thread’s DurableObject instance with methods to access data. You can extend this with custom methods. context.metadata — Thread metadata from the database

ThreadInstance Methods

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

getMessages

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

getLogs

Retrieve execution logs:
const logs = await instance.getLogs(
  limit,   // Max logs (default: 100)
  offset,  // Skip count (default: 0)
  order    // 'asc' or 'desc' (default: 'asc')
);

getThreadMeta

Get metadata for a specific thread:
const meta = await instance.getThreadMeta(threadId);

shouldStop

Check if execution should be stopped:
const stopped = await instance.shouldStop();

ThreadMetadata

The metadata object contains:
interface ThreadMetadata {
  id: string;            // Thread UUID
  agent_id: string;      // Agent name from TypeScript config
  user_id: string | null; // Optional user identifier
  created_at: number;    // Creation timestamp
}

Request Handling

Reading Request Body

export default defineThreadEndpoint(async (req, { instance }) => {
  // 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, { instance }) => {
  const url = new URL(req.url);
  const limit = parseInt(url.searchParams.get('limit') || '10');
  const format = url.searchParams.get('format') || 'json';

  // ...
});

Headers

export default defineThreadEndpoint(async (req, { instance }) => {
  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, { instance, metadata }) => {
  try {
    const { messages } = await instance.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 instance.getMessages(50);

// Avoid - loading entire history for large threads
const { messages } = await instance.getMessages(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