# GraphQL API

> schema.graphqls surface: Query, Mutation, and Subscription operations for flows, assistants, providers, prompts, API tokens, knowledge, usage stats, and real-time log streams over WebSocket.

- Repository: vxcontrol/pentagi
- GitHub: https://github.com/vxcontrol/pentagi
- Human docs: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5
- Complete Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/llms-full.txt

## Source Files

- `backend/pkg/graph/schema.graphqls`
- `backend/pkg/graph/schema.resolvers.go`
- `backend/pkg/graph/subscriptions/controller.go`
- `backend/pkg/server/services/graphql.go`
- `frontend/graphql-schema.graphql`
- `frontend/src/lib`
- `README.md`

---

---
title: "GraphQL API"
description: "schema.graphqls surface: Query, Mutation, and Subscription operations for flows, assistants, providers, prompts, API tokens, knowledge, usage stats, and real-time log streams over WebSocket."
---

PentAGI exposes a single gqlgen GraphQL endpoint at **`/api/v1/graphql`**. The schema lives in `backend/pkg/graph/schema.graphqls` and covers flow execution, assistants, providers, prompts, API tokens, knowledge documents, usage analytics, and real-time subscriptions over WebSocket. Queries and mutations share HTTP transports; subscriptions upgrade to WebSocket on the same path.

## Endpoint and transports

| Surface | Path | Auth |
|---|---|---|
| GraphQL HTTP + WebSocket | `POST/GET/ANY /api/v1/graphql` | Required (`AuthTokenRequired`) |
| GraphQL Playground | `GET /api/v1/graphql/playground` | Optional try-auth (public group) |
| REST (parallel surface) | `/api/v1/*` | Same token/session stack |

Registered transports:

| Transport | Notes |
|---|---|
| `Options`, `GET`, `POST` | Standard query/mutation |
| `MultipartForm` | Max memory **32 MiB** |
| `Websocket` | Keepalive ping every **10s**; origin check against `CorsOrigins` |

Server extensions:

- **Introspection** enabled
- **Automatic persisted queries** (APQ cache size 100)
- **Query document cache** size 1000
- **Fixed complexity limit** `20000`

WebSocket init requires an authenticated user already present on the request context (`GetUserID`). Origin validation allows exact matches, single-`*` wildcards, same-host origins with `http`/`https`/`ws`/`wss` wrappers, and `*` allow-all.

## Authentication and context

Private GraphQL traffic goes through `AuthTokenRequired`, which accepts either:

1. **Bearer API token** — `Authorization: Bearer <token>`
2. **Browser session cookie** — `auth` session with claims `uid`, `prm` (privileges), `tid` (user type), etc.

`ServeGraphql` injects into the request context:

| Context key | Value |
|---|---|
| `userID` | `uint64` from middleware |
| `userType` | session type string (`local`, `oauth`, or token-derived type) |
| `userPermissions` | privilege string slice |

Resolvers call `validatePermission(ctx, "<resource>.<action>")`. Having the `.admin` sibling privilege (for example `flows.admin` when checking `flows.view`) grants admin scope. Flow-scoped operations use `validatePermissionWithFlowID`, which also requires the flow owner unless the caller is admin.

Some mutations additionally require a **user session** (`local` or `oauth`), not a pure API-token identity — notably API token CRUD, favorites, and flow templates.

<Warning>
`createAPIToken` rejects creation when `CookieSigningSalt` is empty or the default value `"salt"`. TTL must be between **60** and **94608000** seconds.
</Warning>

## Architecture

```mermaid
flowchart TB
  subgraph clients [Clients]
    UI["React UI<br/>Apollo + graphql-ws"]
    API["API clients<br/>Bearer token"]
  end

  subgraph http [Gin /api/v1]
    MW["AuthTokenRequired"]
    GQL["GraphqlService<br/>/graphql"]
    PLAY["Playground<br/>/graphql/playground"]
  end

  subgraph gql [gqlgen]
    SCH["schema.graphqls"]
    RES["schema.resolvers.go"]
    CTX["graph context<br/>uid / tid / prm"]
  end

  subgraph runtime [Runtime deps]
    FC["FlowController"]
    PC["ProviderController"]
    KS["KnowledgeStore"]
    SUB["SubscriptionsController"]
    DB[(PostgreSQL)]
  end

  UI -->|HTTP queries/mutations| MW
  UI -->|WS subscriptions| MW
  API --> MW
  MW --> GQL
  GQL --> CTX --> RES
  RES --> SCH
  RES --> FC
  RES --> PC
  RES --> KS
  RES --> SUB
  RES --> DB
  FC --> SUB
  PLAY -.-> GQL
```

Resolver dependencies are injected via `graph.Resolver`: DB, config, logger, token cache, default prompter, provider controller, flow controller, subscriptions controller, knowledge store, and optional text anonymizer.

## Domain enums (high-signal)

| Enum | Values | Used by |
|---|---|---|
| `StatusType` | `created`, `running`, `waiting`, `finished`, `failed` | Flow, Task, Subtask, Assistant |
| `ProviderType` | `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen` | Providers |
| `AgentType` | primary, reporter, generator, refiner, reflector, enricher, adviser, coder, memorist, searcher, installer, pentester, summarizer, tool_call_fixer, assistant | Logs / usage |
| `MessageLogType` | answer, report, thoughts, browser, terminal, file, search, advice, ask, input, done | Message / assistant logs |
| `ToolCallStatus` | `received`, `running`, `finished`, `failed` | Tool call logs |
| `UsageStatsPeriod` | `week`, `month`, `quarter` | Analytics queries |
| `ResultType` | `success`, `error` | Many mutations |
| `TokenStatus` | `active`, `revoked`, `expired` | API tokens |
| `KnowledgeDocType` | `answer`, `guide`, `code` | Knowledge docs |

## Query operations

### Providers and settings

| Field | Args | Returns | Permission |
|---|---|---|---|
| `providers` | — | `[Provider!]!` | `providers.view` |
| `settings` | — | `Settings!` | `settings.view` |
| `settingsProviders` | — | `ProvidersConfig!` | `settings.providers.view` |
| `settingsPrompts` | — | `PromptsConfig!` | `settings.prompts.view` |
| `settingsUser` | — | `UserPreferences!` | `settings.user.view` + user session |

`Settings` fields: `debug`, `askUser`, `version`, `dockerInside`, `isDevelopMode`, `assistantUseAgents`.

`ProvidersConfig` includes readiness flags per provider type, default agent configs, user-defined configs, and model manifests.

### Flows, tasks, logs

| Field | Args | Returns | Permission |
|---|---|---|---|
| `flows` | — | `[Flow!]` | `flows.view` (admin sees all) |
| `flow` | `flowId` | `Flow!` | `flows.view` + ownership |
| `assistants` | `flowId` | `[Assistant!]` | `assistants.view` |
| `tasks` | `flowId` | `[Task!]` | `tasks.view` (+ `subtasks.view` for nested) |
| `flowFiles` | `flowId` | `[FlowFile!]!` | `flow_files.view` |
| `screenshots` | `flowId` | `[Screenshot!]` | `screenshots.view` |
| `terminalLogs` | `flowId` | `[TerminalLog!]` | `termlogs.view` |
| `messageLogs` | `flowId` | `[MessageLog!]` | `msglogs.view` |
| `agentLogs` | `flowId` | `[AgentLog!]` | `agentlogs.view` |
| `searchLogs` | `flowId` | `[SearchLog!]` | `searchlogs.view` |
| `vectorStoreLogs` | `flowId` | `[VectorStoreLog!]` | `vecstorelogs.view` |
| `toolCallLogs` | `flowId` | `[ToolCallLog!]` | `toolcalls.view` |
| `assistantLogs` | `flowId`, `assistantId` | `[AssistantLog!]` | `assistantlogs.view` |

`Flow` includes `id`, `title`, `status`, optional `terminals`, `provider`, timestamps. Terminals load only when the caller has `containers.view`.

### Analytics

All require `usage.view` (flow-scoped variants also check flow ownership).

**Token usage**

| Field | Args | Returns |
|---|---|---|
| `usageStatsTotal` | — | `UsageStats!` |
| `usageStatsByPeriod` | `period` | `[DailyUsageStats!]!` |
| `usageStatsByProvider` | — | `[ProviderUsageStats!]!` |
| `usageStatsByModel` | — | `[ModelUsageStats!]!` |
| `usageStatsByAgentType` | — | `[AgentTypeUsageStats!]!` |
| `usageStatsByFlow` | `flowId` | `UsageStats!` |
| `usageStatsByAgentTypeForFlow` | `flowId` | `[AgentTypeUsageStats!]!` |
| `usageStatsByModelAgentsForFlow` | `flowId` | `[ModelAgentsUsageStats!]!` |

`UsageStats` fields: `totalUsageIn/Out`, `totalUsageCacheIn/Out`, `totalUsageCostIn/Out`.

**Toolcalls / flows / execution**

| Field | Returns |
|---|---|
| `toolcallsStatsTotal` | `ToolcallsStats!` |
| `toolcallsStatsByPeriod(period)` | `[DailyToolcallsStats!]!` |
| `toolcallsStatsByFunction` | `[FunctionToolcallsStats!]!` |
| `toolcallsStatsByFlow(flowId)` | `ToolcallsStats!` |
| `toolcallsStatsByFunctionForFlow(flowId)` | `[FunctionToolcallsStats!]!` |
| `flowsStatsTotal` | `FlowsStats!` |
| `flowsStatsByPeriod(period)` | `[DailyFlowsStats!]!` |
| `flowStatsByFlow(flowId)` | `FlowStats!` |
| `flowsExecutionStatsByPeriod(period)` | `[FlowExecutionStats!]!` |

### API tokens, templates, resources, knowledge

| Field | Args | Returns | Permission |
|---|---|---|---|
| `apiToken` | `tokenId` | `APIToken` | `settings.tokens.view` + user session |
| `apiTokens` | — | `[APIToken!]!` | same |
| `flowTemplate` | `templateId` | `FlowTemplate` | `templates.view` + user session |
| `flowTemplates` | — | `[FlowTemplate!]!` | same |
| `resources` | `path`, `recursive` | `[UserResource!]!` | `resources.view` |
| `knowledgeDocuments` | `filter`, `withContent` | `[KnowledgeDocument!]!` | `knowledge.view` |
| `knowledgeDocument` | `id` | `KnowledgeDocument!` | `knowledge.view` |
| `searchKnowledge` | `query`, `filter`, `limit` | `[KnowledgeDocumentWithScore!]!` | `knowledge.search` |

When `withContent` is `false`, document `content` is returned empty to reduce payload size. Admins search/list all documents; non-admins are scoped to their `userId`.

## Mutation operations

### Flow lifecycle

| Field | Args | Returns | Permission |
|---|---|---|---|
| `createFlow` | `modelProvider`, `input`, `resourceIds?` | `Flow!` | `flows.create` |
| `putUserInput` | `flowId`, `input`, `modelProvider?`, `resourceIds?` | `ResultType!` | `flows.edit` |
| `stopFlow` | `flowId` | `ResultType!` | `flows.edit` |
| `finishFlow` | `flowId` | `ResultType!` | `flows.edit` |
| `deleteFlow` | `flowId` | `ResultType!` | `flows.delete` |
| `renameFlow` | `flowId`, `title` | `ResultType!` | `flows.edit` |

`createFlow` requires non-empty `modelProvider` and `input`, resolves the provider via `ProvidersCtrl.GetProvider`, then `Controller.CreateFlow`. Optional `resourceIds` attach user resources when the caller has `resources.view`.

### Assistants

| Field | Args | Returns | Permission |
|---|---|---|---|
| `createAssistant` | `flowId`, `modelProvider`, `input`, `useAgents`, `resourceIds?` | `FlowAssistant!` | `assistants.create` (+ flow ownership when `flowId` set) |
| `callAssistant` | `flowId`, `assistantId`, `input`, `useAgents`, `resourceIds?` | `ResultType!` | `assistants.edit` |
| `stopAssistant` | `flowId`, `assistantId` | `Assistant!` | `assistants.edit` |
| `deleteAssistant` | `flowId`, `assistantId` | `ResultType!` | `assistants.delete` |

### Providers and prompts

| Field | Args | Returns | Permission |
|---|---|---|---|
| `testAgent` | `type`, `agentType`, `agent` | `AgentTestResult!` | `settings.providers.view` |
| `testProvider` | `type`, `agents` | `ProviderTestResult!` | `settings.providers.view` |
| `createProvider` | `name`, `type`, `agents` | `ProviderConfig!` | `settings.providers.edit` |
| `updateProvider` | `providerId`, `name`, `agents` | `ProviderConfig!` | `settings.providers.edit` |
| `deleteProvider` | `providerId` | `ResultType!` | `settings.providers.edit` |
| `validatePrompt` | `type`, `template` | `PromptValidationResult!` | `settings.prompts.edit` |
| `createPrompt` | `type`, `template` | `UserPrompt!` | `settings.prompts.edit` |
| `updatePrompt` | `promptId`, `template` | `UserPrompt!` | `settings.prompts.edit` |
| `deletePrompt` | `promptId` | `ResultType!` | `settings.prompts.edit` |

`AgentConfigInput` supports model, token limits, sampling (`temperature`, `topK`, `topP`, penalties), `reasoning` (`effort`, `maxTokens`), and `price`.

`PromptValidationErrorType`: `syntax_error`, `unauthorized_variable`, `rendering_failed`, `empty_template`, `variable_type_mismatch`, `unknown_type`.

### Tokens, favorites, templates, knowledge, anonymize

| Field | Notes | Permission |
|---|---|---|
| `createAPIToken` | Returns `APITokenWithSecret` (secret once) | `settings.tokens.create` + user session |
| `updateAPIToken` | name / status | `settings.tokens.edit` + user session |
| `deleteAPIToken` | returns `Boolean!` | `settings.tokens.delete` + user session |
| `addFavoriteFlow` / `deleteFavoriteFlow` | favorites list | `settings.user.edit` + user session |
| `createFlowTemplate` / `updateFlowTemplate` / `deleteFlowTemplate` | title + text | `templates.*` + user session |
| `createKnowledgeDocument` | re-embeds content | `knowledge.create` |
| `updateKnowledgeDocument` | non-null fields re-embed | `knowledge.edit` |
| `deleteKnowledgeDocument` | | `knowledge.delete` |
| `anonymizeText` | uses cloud anonymizer when configured | `anonymize.call` |

## Subscription operations

Subscriptions use the same `/api/v1/graphql` URL over WebSocket (`graphql-ws` protocol on the frontend). Publishers fan out through `SubscriptionsController` with buffered channels (length **50**, send timeout **5s**).

### Flow-scoped streams

Require the matching `*.subscribe` privilege and flow ownership (unless admin).

| Field | Payload | Permission |
|---|---|---|
| `taskCreated` / `taskUpdated` | `Task!` | `tasks.subscribe` |
| `assistantCreated` / `Updated` / `Deleted` | `Assistant!` | `assistants.subscribe` |
| `flowFileAdded` / `Updated` / `Deleted` | `FlowFile!` | `flow_files.subscribe` |
| `screenshotAdded` | `Screenshot!` | `screenshots.subscribe` |
| `terminalLogAdded` | `TerminalLog!` | `termlogs.subscribe` |
| `messageLogAdded` / `Updated` | `MessageLog!` | `msglogs.subscribe` |
| `agentLogAdded` | `AgentLog!` | `agentlogs.subscribe` |
| `searchLogAdded` | `SearchLog!` | `searchlogs.subscribe` |
| `vectorStoreLogAdded` | `VectorStoreLog!` | `vecstorelogs.subscribe` |
| `toolCallLogAdded` / `Updated` | `ToolCallLog!` | `toolcalls.subscribe` |
| `assistantLogAdded` / `Updated` | `AssistantLog!` | `assistantlogs.subscribe` |

`assistantLogUpdated` supports streaming chunks via `appendPart: Boolean!`. The UI concatenates partial `message` / `result` / `thinking` fields.

### Global / user-scoped streams

| Field | Permission | Admin behavior |
|---|---|---|
| `flowCreated` / `Updated` / `Deleted` | `flows.subscribe` | Admin uses broadcast channels; others receive own flows |
| `providerCreated` / `Updated` / `Deleted` | `settings.providers.subscribe` | user-scoped |
| `apiTokenCreated` / `Updated` / `Deleted` | `settings.tokens.subscribe` | user-scoped |
| `settingsUserUpdated` | `settings.user.subscribe` | user-scoped |
| `flowTemplateCreated` / `Updated` / `Deleted` | `templates.subscribe` | user-scoped |
| `resourceAdded` / `Updated` / `Deleted` | `resources.subscribe` | admin can use admin variants |
| `knowledgeDocumentCreated` / `Updated` / `Deleted` | `knowledge.subscribe` | admin can use admin variants |

## Request examples

<RequestExample>
```bash
# Query settings (session cookie or Bearer token)
curl -sS -X POST 'https://localhost:8443/api/v1/graphql' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <API_TOKEN>' \
  -d '{"query":"query { settings { version debug askUser dockerInside isDevelopMode assistantUseAgents } }"}'
```
</RequestExample>

<ResponseExample>
```json
{
  "data": {
    "settings": {
      "version": "…",
      "debug": false,
      "askUser": true,
      "dockerInside": true,
      "isDevelopMode": false,
      "assistantUseAgents": false
    }
  }
}
```
</ResponseExample>

<RequestExample>
```graphql
# Create a flow
mutation CreateFlow($provider: String!, $input: String!) {
  createFlow(modelProvider: $provider, input: $input) {
    id
    title
    status
    provider { name type }
    createdAt
  }
}
```
</RequestExample>

Variables:

```json
{
  "provider": "openai",
  "input": "Perform a scoped web application reconnaissance against example.com"
}
```

<RequestExample>
```graphql
# Live terminal + message stream for a flow
subscription FlowLogs($flowId: ID!) {
  terminalLogAdded(flowId: $flowId) {
    id type text terminal createdAt
  }
}
```
</RequestExample>

`modelProvider` is a **provider name** (built-in type name or user-defined provider profile name), not a raw model ID. Resolve available names via `providers` or `settingsProviders`.

## Frontend client

The React app uses Apollo Client (`frontend/src/lib/apollo.ts`):

| Concern | Behavior |
|---|---|
| HTTP endpoint | `${origin}/api/v1/graphql` with `credentials: 'include'` |
| WebSocket URL | `ws(s)://host/api/v1/graphql` via `graphql-ws` |
| Split link | Subscriptions → WS; everything else → HTTP |
| Retry | Infinite reconnect with exponential backoff (cap **30s**) |
| Cache | `InMemoryCache` with subscription-driven list updates |
| Streaming | `assistantLogUpdated` + `appendPart` accumulated client-side (50 ms throttle) |
| Auth errors | Dispatches `auth:refresh` on 401/403 / `UNAUTHENTICATED` / `FORBIDDEN` |

Frontend schema mirror: `frontend/graphql-schema.graphql`. Regenerated types: `pnpm run graphql:generate` (`graphql-codegen.ts` → `frontend/src/graphql/types.ts`).

## Schema ownership and codegen

:::files
backend/
  pkg/graph/
    schema.graphqls          # Source of truth
    schema.resolvers.go      # Resolver implementations
    generated.go             # gqlgen exec
    model/models_gen.go      # Generated models
    context.go               # Auth helpers
    subscriptions/           # Pub/sub controller
  gqlgen/gqlgen.yml
  pkg/server/services/graphql.go
  pkg/server/router.go       # Route mount
frontend/
  graphql-schema.graphql     # Client schema copy
  graphql-codegen.ts
  src/lib/apollo.ts
:::

After schema changes:

```bash
# Backend (from backend/)
go run github.com/99designs/gqlgen --config ./gqlgen/gqlgen.yml

# Frontend (from frontend/)
pnpm run graphql:generate
```

`gqlgen.yml` maps GraphQL `ID` primarily to Go `Int64` (with fallbacks). Input types for agent config reuse the same Go structs as output models.

## Permission map (compact)

| Resource prefix | Typical actions |
|---|---|
| `flows` | view, create, edit, delete, subscribe (+ `.admin`) |
| `tasks`, `subtasks` | view, subscribe |
| `assistants` | view, create, edit, delete, subscribe |
| `*_logs` / `termlogs` / `msglogs` / `agentlogs` / `searchlogs` / `vecstorelogs` / `toolcalls` / `screenshots` / `flow_files` | view, subscribe |
| `providers` | view |
| `settings` | view |
| `settings.providers` | view, edit, subscribe |
| `settings.prompts` | view, edit |
| `settings.tokens` | view, create, edit, delete, subscribe |
| `settings.user` | view, edit, subscribe |
| `templates` | view, create, edit, delete, subscribe |
| `resources` | view, subscribe |
| `knowledge` | view, create, edit, delete, search, subscribe |
| `usage` | view |
| `containers` | view (gates terminal hydration on Flow) |
| `anonymize` | call |

## Operational constraints and failure modes

| Condition | Behavior |
|---|---|
| Missing/invalid auth | HTTP 403 from Gin auth middleware (`ErrAuthRequired`) |
| Missing privilege | GraphQL error: `requested permission '…' not found` |
| Non-owner flow access | `not permitted` |
| Empty `modelProvider` / `input` on create | Resolver validation errors |
| Provider not found | Error from `ProvidersCtrl.GetProvider` |
| Complexity > 20000 | gqlgen complexity rejection |
| WebSocket origin not allowed | Upgrade rejected by `CheckOrigin` |
| Anonymizer unavailable | `anonymizeText` → `anonymizer is not available` |
| Default cookie salt | Token creation disabled |
| API token used for token CRUD | Rejected: non-user session not allowed |
| Subscription send backlog | Channel buffer 50; send drops after 5s timeout |

<Tip>
Playground is mounted under the public group at `/api/v1/graphql/playground` and points the GraphiQL client at `/api/v1/graphql`. Authenticated operations still require a valid session cookie or Bearer header on the actual GraphQL requests.
</Tip>

## Relation to REST

GraphQL is the primary real-time and UI surface; REST under `/api/v1` covers overlapping CRUD (flows, tasks, files, tokens, knowledge, etc.) and Swagger at `/api/v1/swagger`. Prefer GraphQL for:

- Multi-entity reads with nested selection sets
- Live log / status streams
- Provider testing and prompt validation in the settings UI

Prefer REST for simple scripted file/resource binary operations and OpenAPI tooling.

## Next

<CardGroup>
  <Card title="REST API" href="/rest-api">
    Gin routes under `/api/v1`, overlapping CRUD resources, and Swagger.
  </Card>
  <Card title="Authentication and API tokens" href="/auth-and-api-tokens">
    Session login, OAuth, Bearer tokens, and permission-scoped token lifecycle.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Execution hierarchy, status states, and putUserInput / stop / finish boundaries.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Provider wiring and profiles used by `modelProvider` and settings mutations.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Vector knowledge documents, embeddings, and searchKnowledge filters.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    gqlgen regeneration, frontend graphql:generate, and contributor workflows.
  </Card>
</CardGroup>
