Skip to content

AI Connectors

Zenvara treats an LLM call the same way it treats a database query: a typed step with declared inputs and outputs, logged prompt and response, composing with retries, secrets, and audit. There is one connector for chat completions — ai.* — and the model vendor is a connection field, not a connector choice: the environment decides whether Provider means Anthropic, OpenAI, Google, or a self-hosted model, the same way it decides which database db resolves to.

Connectorinvoke:What it does
AIai.*call — a chat-completion request to the configured provider; typed response with token counts and, for tool-using flows, toolCalls.
AI Agentai-agent.*invoke, start-session — run an autonomous coding agent (Claude Code, OpenCode, Gemini CLI, Codex CLI) as a flow step, or spawn an interactive session with quota management and human-assist.
Knowledgeknowledge.*index, retrieve, plan-sync, forget — chunk, embed, and query content through a swappable vector index (RAG).

ai and ai-agent answer different questions: ai is a token-billed, conversational API call — the shape of a single completion. ai-agent is process-based and agentic — it drives a whole coding session, with its own working directory, turn limit, and quota management.

Terminal window
using:
- environment/prod
- zenvara/ai
output:
content: !str
steps:
- $summary:
invoke: ai.call
on: ai
with:
SystemPrompt: "You are a concise analyst."
Prompt: "Summarise these orders in two paragraphs:\n${orders.rows}"
- return:
content: "${summary.content}"

Outputs: content (generated text), finishReason, inputTokens, outputTokens, totalTokens, toolCalls (when function-calling), cacheCreationInputTokens / cacheReadInputTokens (Anthropic prompt-cache accounting — 0 for other providers). Reference them as ${summary.content}, ${summary.outputTokens}.

The model vendor lives on the connection, not in the flow. A zenvara/ai connection carries a Provider field — anthropic, openai, google, or local — plus whatever that vendor needs:

config/connections/ai-anthropic.connection.yaml
type: zenvara/ai
kind: production
Provider: anthropic
ApiKey: "${secret:ai/api-key}"
Model: "claude-sonnet-4-6"
MaxTokens: 4096
TimeoutSeconds: 120
EnableCaching: true # Anthropic prompt-cache breakpoints; ignored by other providers

Flip Provider to openai, point Model at gpt-4.1, and reference an OpenAI key instead — same fields, same secret-reference shape. Nothing in the ai worked example above changes: invoke: ai.call is the verb whichever vendor answers it. See Environments for how an alias like ai picks which connection is live, and Secrets for the ${secret:...} form.

The strongest form of the switch: point Provider at local and the same flow runs against Ollama, LM Studio, vLLM, llama.cpp --server, or an OpenRouter gateway — anything speaking the OpenAI-compatible chat-completions wire format.

Terminal window
type: zenvara/ai
Provider: local
BaseUrl: "http://localhost:11434/v1" # Ollama default; LM Studio: http://localhost:1234/v1
ApiKey: "" # Ollama needs none — a placeholder is substituted internally
Model: "qwen2.5:7b"

local requires BaseUrl — the manifest enforces it. A Provider: local connection with no BaseUrl is rejected before any request is built, not discovered later at run time.

ai calls are single-shot completions; ai-agent runs a full coding agent as a subprocess — Claude Code, OpenCode, Gemini CLI, or Codex CLI — with quota management, stuck-detection, and human-assist when it needs a decision. Same provider-transparency shape, one level up: Provider picks the agent (claude, opencode, gemini, codex).

Terminal window
using:
- environment/prod
- zenvara/ai-agent
output:
output: !str
steps:
- $task:
invoke: ai-agent.invoke
on: agent
with:
Task: "Refactor the auth module to use the new session store"
WorkingDirectory: "/repo"
TimeoutMinutes: 30
- return:
output: "${task.output}"

Outputs: success, status, output, errorOutput, exitCode, durationSeconds, provider — plus Claude-only fields (sessionId, quotaWaitSeconds, stats) when Provider: claude.

start-session spawns the agent as a long-running interactive process instead of waiting for completion — it returns immediately with a sessionId and a terminalUrl for live attach through Studio’s terminal.

Two constraints worth flagging before the actions:

  • Embeddings use a separate connection from the chat ai connection. The embedding-provider enum is openai | google | local — Anthropic is intentionally absent, since it ships no embeddings API.
  • An index is pinned to one embedding model. Querying it with a different model is an error — vectors from different models aren’t comparable.
Terminal window
using:
- environment/prod
- zenvara/knowledge
output:
chunks: !obj-list
steps:
- $ingest:
invoke: knowledge.index
on: embeddings
with:
Content: "${page.body}"
Collection: "handbook"
SourceSystem: "confluence"
SourceId: "${page.id}"
SourceUri: "${page.url}"
- $hits:
invoke: knowledge.retrieve
on: embeddings
with:
Query: "What did we decide about retention policy?"
Collection: "handbook"
TopK: 5
- return:
chunks: "${hits.chunks}"

Outputs: index returns chunksIndexed; retrieve returns chunks — a ranked array of { content, score, collection, source }, each carrying its provenance for citation. plan-sync reconciles a cheap live source manifest against what’s already indexed (toIndex / toDelete) for incremental re-index jobs; forget drops every chunk for one source.

Four single-action connectors, one shape (.search) — and a different category (Search, not AI):

Connectorinvoke:What it does
Web Search (DuckDuckGo)web-search-duckduckgo.searchDuckDuckGo’s lite HTML endpoint. No API key.
Web Search (Google)web-search-google.searchGoogle Custom Search JSON API. Needs ApiKey + SearchEngineId (the Custom Search Engine’s cx id).
Web Search (Ollama)web-search-ollama.searchOllama’s hosted web-search API at ollama.com. Needs ApiKey.
Web Search (StackOverflow)web-search-stackoverflow.searchStack Exchange API v2.3. ApiKey optional (raises rate limits); Site selects a Stack Exchange community shortname (security, math, …) — not an arbitrary domain.
Terminal window
using:
- zenvara/web-search-duckduckgo
output:
data: !obj-list
steps:
- $hits:
invoke: web-search-duckduckgo.search
with:
Query: "F# FS0039 value not defined"
- return:
data: "${hits.data.data}"

Outputs are nested: results at ${hits.data.data[*]} (title, url, snippet), pagination at ${hits.data.pagination} (totalItems, page, pageSize, itemCount, hasMore) — not a flat ${hits.data[*]}.

ai.call supports structured content blocks (text, tool_use, tool_result) and a Tools list of tool definitions (Name, Description, InputSchema). This is provider-transparent too: InputSchema maps to whatever the provider calls it (input_schema for Anthropic, parameters for OpenAI/Gemini) at request build. A flow drives a full agentic tool loop by feeding each tool result back as a typed message — the model’s toolCalls output steers the next step.

Because the response is a typed value, the next step consumes it like any other:

Terminal window
- $tickets:
invoke: jira.search-issues
with:
jql: "${summary.content}" # AI output → next connector's input

That composition — AI output flowing into a database query, an HTTP call, or a filter: — is the point of a typed AI connector. See Authoring Flows for how values flow between steps.