> ## Documentation Index
> Fetch the complete documentation index at: https://synthetiq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Add an AI agent

> Configure the built-in AI agent with an LLM service and custom system prompt

Every Synthetiq app can include an in-app AI agent that understands and can interact with the app's API surface, data model, and connected services. The agent always assumes the scopes and permissions of the user using it, as defined in `scopes.json` (see [Add access control](/docs/platform-docs/app-framework/creating/access-control)).

The framework handles the agent loop, tool execution, chat history, context compaction, streaming, and scope enforcement. It auto-generates system prompts from the app's procedures, database schema, and connected services. You simply provide the LLM integration and customize the agent's instructions.

## LLM integration

The AI agent needs an LLM service. Install the Anthropic service client:

```bash theme={null}
pnpm add @synthetiq/services-anthropic-client
```

Configure it in `src/server/lib/agentLLM.ts` (this file is scaffolded when you create the app):

```typescript theme={null}
import LLMClient from "@synthetiq/services-anthropic-client";
import { throwIfChunkError } from "@synthetiq/app-framework/server";

export async function* streamLLM(
  messages: Array<{ role: 'user' | 'assistant'; content: string }>,
  systemPrompt: string,
): AsyncGenerator<string> {
  const client = new LLMClient();

  for await (const chunk of client.streamMessageStream({
    model: "claude-sonnet-4-20250514",
    max_tokens: 8192,
    system: [{ type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } }],
    messages,
  })) {
    throwIfChunkError(chunk);
    yield chunk;
  }
}
```

The client handles authentication automatically using platform-managed credentials — app code never sees API keys.

## System prompt

Customize the agent's personality for the task tracker in `src/server/lib/appSystemPrompt.ts`:

```typescript theme={null}
export const APP_SYSTEM_PROMPT = `You are a task management assistant that helps users
track tasks, plan work, and stay organized.

You can:
- Create and update tasks
- List tasks filtered by status or priority
- Help users plan their work and prioritize tasks

Always be concise and action-oriented.`;
```

The framework automatically appends the API manifest, database schema, and service metadata to this prompt — so the agent knows about every procedure, table, and service tool without you listing them manually.

## Scope-aware tool access

The agent only sees and can access procedures that the authenticated user has permission to call. A user with the `tasks:viewAll` scope sees the `getAllTasks` tool; a regular user only sees `getMyTasks`. This filtering happens automatically based on the user's roles.

Additionally, you can give the AI agent direct access to service tools. For example, to let admins with the `slack:post` scope ask the agent to post Slack messages, add the scope and update the `services` section of `scopes.json`:

```json theme={null}
{
  "scopes": [
    ...
    { "name": "slack:post", "description": "Post messages to Slack via the AI agent" }
  ],
  "tables": { ... },
  "services": {
    "slack": {
      "requiredScopes": ["slack:post"],
      "tools": {}
    }
  }
}
```

The `requiredScopes` array gates access to the entire Slack service — only users with the `slack:post` scope can use any Slack tools through the AI agent. You can also gate specific tools individually via the `tools` object. For more details, see the [Access control reference](/docs/platform-docs/app-framework/reference/access-control).

## Chat UI

The framework provides all the components needed to build an in-app AI chat. Here's an example:

```typescript theme={null}
// src/web/pages/Chat.tsx
import { useMemo } from "react";
import {
  useAgentChat, ChatMessageComponent as ChatMessage,
  ChatInput, ToolCallsPanel, MessageType,
} from "@synthetiq/app-framework/web";
import type { ChatMessageData as ChatMessageType } from "@synthetiq/app-framework/web";

export function ChatPage() {
  const {
    sessions, currentSession, messages,
    isProcessing, processingStage, isLoading,
    sendMessage, cancelRequest, selectSession,
  } = useAgentChat({});

  const groupedMessages = useMemo(() => {
    type MessageGroup =
      | { type: "user"; message: ChatMessageType }
      | { type: "tools"; messages: ChatMessageType[] }
      | { type: "assistant"; message: ChatMessageType };

    const groups: MessageGroup[] = [];
    let currentToolGroup: ChatMessageType[] = [];

    for (const msg of messages) {
      if (msg.type === MessageType.USER_MESSAGE) {
        if (currentToolGroup.length > 0) {
          groups.push({ type: "tools", messages: currentToolGroup });
          currentToolGroup = [];
        }
        groups.push({ type: "user", message: msg });
      } else if (msg.type === MessageType.ASSISTANT_MESSAGE) {
        if (currentToolGroup.length > 0) {
          groups.push({ type: "tools", messages: currentToolGroup });
          currentToolGroup = [];
        }
        groups.push({ type: "assistant", message: msg });
      } else {
        currentToolGroup.push(msg);
      }
    }
    if (currentToolGroup.length > 0) {
      groups.push({ type: "tools", messages: currentToolGroup });
    }
    return groups;
  }, [messages]);

  const handleSend = async (content: string) => {
    if (currentSession) {
      await sendMessage(content, currentSession.id);
    } else {
      await sendMessage(content, crypto.randomUUID(), true);
    }
  };

  return (
    <div className="flex flex-col h-full">
      <div className="flex items-center gap-2 p-2 border-b">
        <select
          value={currentSession?.id || ""}
          onChange={(e) => e.target.value && selectSession(e.target.value)}
        >
          <option value="" disabled>Select chat...</option>
          {sessions.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
        </select>
        <button onClick={() => selectSession("")}>New Chat</button>
      </div>
      <div className="flex-1 overflow-y-auto p-4 pb-24 space-y-4">
        {groupedMessages.map((group, i) => {
          const isLastGroup = i === groupedMessages.length - 1;
          if (group.type === "user") {
            return (
              <div key={`user-${i}`}>
                <ChatMessage message={group.message} />
                {isProcessing && isLastGroup && (
                  <ToolCallsPanel messages={[]} isStreaming={true} processingStage={processingStage} />
                )}
              </div>
            );
          }
          if (group.type === "tools") {
            return (
              <ToolCallsPanel
                key={`tools-${i}`}
                messages={group.messages}
                isStreaming={isProcessing && isLastGroup}
                processingStage={isLastGroup ? processingStage : null}
              />
            );
          }
          if (group.type === "assistant") {
            return <ChatMessage key={`assistant-${i}`} message={group.message} />;
          }
          return null;
        })}
      </div>
      <ChatInput
        onSend={handleSend}
        disabled={isProcessing || isLoading}
        isProcessing={isProcessing}
        onCancel={cancelRequest}
      />
    </div>
  );
}
```

The `useAgentChat` hook manages sessions, message history, streaming, and state. The framework provides `ChatMessageComponent`, `ChatInput`, `ToolCallsPanel`, and `ProcessingStage` — you arrange and style them as needed.

## Capabilities

With the task tracker's procedures, database schema, and connected services, users can now ask the agent things like:

* "What tasks do I have this week?"
* "Create a high-priority task called 'Update API docs'"
* "Mark the 'Fix login bug' task as completed"
* "How many active tasks do I have?"

Admins with additional scopes can also ask:

* "Show me all tasks across the team"
* "Who has the most overdue tasks?"
* "Post a summary of today's completed tasks to Slack" (requires `slack:post` scope)
