What is Symfony AI?

Symfony AI is the Symfony project's own set of PHP components for working with language models: one API for many model providers, agents that call your code as tools, vector stores for retrieval, and bundles that wire all of it into a Symfony application. It lives in the symfony/ai monorepo and is documented on symfony.com and ai.symfony.com.

The timeline matters for planning. The initiative was announced in August 2025, the Symfony team took on the official MCP PHP SDK with the PHP Foundation in September 2025, and the first tagged release, v0.1.0, came out on 24 December 2025. As of October 2026 the latest release is v0.14.1 (27 September 2026), with a new minor version roughly every month.

This guide is for teams deciding whether to put Symfony AI into a real product. If you want the path that uses only long-stable components (HttpClient, Messenger, the rate limiter and Doctrine with pgvector), read our guide on how to add AI to a Symfony app first. Here we cover the Symfony AI components themselves and what you still need to add around them.

Which components make up Symfony AI, and how stable are they?

Symfony AI has five components and two bundles. Each one is a separate Composer package, and each provider or database integration is a further "bridge" package you install only if you need it.

Package What it does Typical use in a product
Platform (symfony/ai-platform) One interface to call OpenAI, Anthropic, Gemini, Azure, Bedrock, Mistral, Ollama and many more Summaries, extraction, chat, embeddings
Agent (symfony/ai-agent) Tool calling loop, input and output processors, memory An assistant that reads orders or drafts replies through your services
Store (symfony/ai-store) Vector store abstraction with indexers and retrievers RAG over help articles, documents or tickets
Chat (symfony/ai-chat) Conversations with stored history Multi-turn chat with persistence
Mate (symfony/ai-mate) A command-line tool that lets coding agents inspect a running Symfony app Development only
AI Bundle (symfony/ai-bundle) Wires Platform, Agent, Store and Chat from config/packages/ai.yaml The usual entry point
MCP Bundle (symfony/mcp-bundle) MCP server and client on top of the official mcp/sdk Exposing your app to Claude, ChatGPT or Cursor

As of October 2026 (v0.14.1), the requirements and status are:

  • PHP 8.2 or newer for the components.
  • Symfony 7.3 or newer for the bundles, including 7.4 LTS and 8.x.
  • Every package is marked experimental. The README of each component says so, which means it is not covered by Symfony's backward compatibility promise. Classes, config keys and method signatures can change between minor versions, and they do: v0.14 alone moved several platform bridges to a new asynchronous job model and introduced new factories for most HTTP-based stores.
  • The MCP Bundle depends on mcp/sdk ^0.8. The SDK has its own release line (v0.9.0 shipped on 18 October 2026), so expect the bundle and SDK versions to move together.

For a 0.x package, Composer's caret constraint ^0.14 already means "0.14.x only", so a plain composer update will not jump you to 0.15. Keep it that way and upgrade on purpose.

How do you install the AI Bundle and configure OpenAI or Claude?

Install the bundle plus one bridge per provider. The bundle refuses to boot with a clear error if you configure a provider whose bridge is missing.

composer require symfony/ai-bundle symfony/ai-open-ai-platform symfony/ai-anthropic-platform

Every platform uses Symfony HttpClient, and the bundle lets you pass your own client service per platform. That is where production settings belong: timeouts and retries for rate limits and server errors.

# config/packages/framework.yaml
framework:
  http_client:
    scoped_clients:
      ai.http_client:
        scope: 'https://api\.(openai|anthropic)\.com'
        timeout: 120
        retry_failed:
          max_retries: 2
          delay: 2000
          multiplier: 3
# config/packages/ai.yaml
ai:
  platform:
    openai:
      api_key: '%env(OPENAI_API_KEY)%'
      http_client: 'ai.http_client'
    anthropic:
      api_key: '%env(ANTHROPIC_API_KEY)%'
      http_client: 'ai.http_client'
      cache_retention: short   # Anthropic prompt caching: none, short or long

Each entry becomes a service, ai.platform.openai and ai.platform.anthropic. You can test the connection before writing any code with php bin/console ai:platform:invoke anthropic <model> "Say hello".

Model names come from each bridge's model catalog. The catalogs in v0.14.1 include current models such as gpt-5-mini and claude-sonnet-5; if your provider ships a model the catalog does not know yet, the bundle lets you add it to the catalog in config instead of waiting for a release.

How do you build a Symfony AI agent with tools?

An agent is a platform plus a model, a system prompt and a list of tools. Tools are ordinary services marked with #[AsTool], and the bundle builds the JSON schema the model sees from the method signature and docblock.

# config/packages/ai.yaml (continued)
ai:
  agent:
    support:
      platform: 'ai.platform.anthropic'
      model: 'claude-sonnet-5'
      prompt:
        text: 'You help support staff answer questions about orders. Use tools, never guess order data.'
      tools:
        - 'App\Ai\Tool\FindOrder'
      max_tool_calls: 8   # default is 50, far too many for a support bot
namespace App\Ai\Tool;

use App\Repository\OrderRepository;
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool;
use Symfony\AI\Platform\Contract\JsonSchema\Attribute\Schema;

#[IsGrantedTool('ROLE_SUPPORT')]
#[AsTool('find_order', 'Returns status, items and shipping data of one order by its number')]
final class FindOrder
{
    public function __construct(private OrderRepository $orders) {}

    /**
     * @param string $number Order number as printed on the invoice, for example A-10423
     */
    public function __invoke(#[Schema(pattern: '^A-[0-9]{4,8}$')] string $number): array
    {
        $order = $this->orders->findOneByNumber($number);

        return $order ? $order->toAiSummary() : ['error' => 'Order not found'];
    }
}

Then inject the agent by its service id and call it:

use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class SupportAssistant
{
    public function __construct(
        #[Autowire(service: 'ai.agent.support')] private AgentInterface $agent,
    ) {}

    public function answer(string $question): string
    {
        return $this->agent->call(new MessageBag(Message::ofUser($question)))->getContent();
    }
}

Three details from practice:

  1. Tools are opt-in. An agent without a tools key gets no tools at all. tools: true gives it every #[AsTool] service in the app, which is rarely what you want once you have more than one agent.
  2. #[IsGrantedTool] uses Symfony Security, so the tool is checked against the current user. In a Messenger worker there is no logged-in user; decide up front which agents run in workers and give them only tools that do not depend on a session.
  3. Return small, plain data from tools. Whatever a tool returns goes back into the context window and is billed as input tokens on every following step of the loop.

The bundle also supports agents as tools of other agents, multi-agent orchestration and tools from remote MCP servers. Start with one agent and one to five tools, measured on real questions, before adding any of that. Our AI agent development services follow the same rule.

How do you set up a vector store with pgvector?

The Store component separates three jobs: a vectorizer turns text into embeddings, an indexer writes documents into a store, and a retriever turns a question into a vector and queries the store. With the Postgres bridge, the store is a pgvector table.

composer require symfony/ai-postgres-store
# config/packages/ai.yaml (continued)
ai:
  store:
    postgres:
      kb:
        dbal_connection: 'doctrine.dbal.default_connection'
        table_name: 'ai_kb_chunk'
        vector_field: 'embedding'
        distance: 'cosine'
        setup_options:
          vector_size: 1536
          index_method: 'hnsw'
          index_opclass: 'vector_cosine_ops'
  vectorizer:
    openai_small:
      platform: 'ai.platform.openai'
      model: 'text-embedding-3-small'
  indexer:
    kb:
      vectorizer: 'ai.vectorizer.openai_small'
      store: 'ai.store.postgres.kb'
  retriever:
    kb:
      vectorizer: 'ai.vectorizer.openai_small'
      store: 'ai.store.postgres.kb'

php bin/console ai:store:setup creates the table and index. Index documents with a tenant id in the metadata, and filter on it in every query:

use Symfony\AI\Store\Document\Metadata;
use Symfony\AI\Store\Document\TextDocument;
use Symfony\Component\Uid\Uuid;

$this->kbIndexer->index(new TextDocument(
    Uuid::v7()->toRfc4122(),
    $article->getText(),
    new Metadata(['tenant_id' => $tenant->getId(), 'article_id' => $article->getId()]),
));

$chunks = $this->kbRetriever->retrieve($question, [
    'limit' => 8,
    'where' => "metadata->>'tenant_id' = :tenant",
    'params' => ['tenant' => (string) $tenant->getId()],
]);

What the store docs do not stress:

  • The default index method is ivfflat. Built on an empty table, as ai:store:setup does, it gives poor recall later. Set index_method: hnsw explicitly, as above.
  • The where option is raw SQL. Never put user input into it; pass values through params.
  • Metadata is a JSONB column with no index on it. Add an expression index on (metadata->>'tenant_id') in a Doctrine migration once the table grows past a few hundred thousand rows.
  • The id column is a UUID, so you cannot reuse integer entity ids; keep the entity id in metadata.
  • Doctrine migrations do not know this table. Add it to schema_filter or the next doctrine:migrations:diff will try to drop it.

Chunking, hybrid search and evaluation work the same as with any stack; see our RAG implementation guide and, if you want a team to build it, RAG development services.

How does the Symfony MCP bundle work?

The MCP Bundle turns your Symfony app into an MCP server that AI clients such as Claude, ChatGPT or Cursor can connect to, over HTTP or STDIO. It can also act as an MCP client, so your own agents can use tools from other servers.

composer require symfony/mcp-bundle
# config/routes.yaml
mcp:
  resource: .
  type: mcp

# config/packages/mcp.yaml
mcp:
  servers:
    app:
      name: 'acme-backoffice'
      transports: { http: true }
      http:
        path: /mcp
      registry:
        tools: ['App\Mcp\']
      session:
        store: cache
        cache_pool: 'cache.mcp.sessions'
namespace App\Mcp;

use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;

final class OrderStatusTool
{
    public function __construct(private OrderRepository $orders) {}

    #[McpTool(name: 'order-status', description: 'Status and ETA of one order for the signed-in customer')]
    public function __invoke(#[Schema(description: 'Order number, for example A-10423')] string $number): array
    {
        // load through the same voter-protected service your API uses
    }
}

Access control is plain Symfony Security: each server has its own route, so a firewall or access_control rule protects it. For OAuth 2.1, which Claude and ChatGPT expect from remote servers, the bundle docs point to an external identity provider or a separate OAuth bundle; it does not issue tokens itself.

Two production details. The default session store is the filesystem under var/cache, which breaks as soon as you run two PHP containers behind a load balancer; use the cache or framework session store backed by Redis, as above. And keep the MCP server's tool list short and its write tools draft-only; our guide to building an MCP server for a SaaS explains tool design, and our MCP server development services cover the build.

What do the Symfony AI docs not tell you about production?

The components give you calls, agents and stores. They do not give you cost control, background execution or a test strategy. These are the pieces we add on every project.

Cost logging per feature

Each platform dispatches events through Symfony's event dispatcher. ResultConvertedEvent fires after every model response is converted, including every step of an agent's tool loop, and the result metadata carries a token_usage object with prompt, completion, cached and thinking tokens when the provider reports them.

use Symfony\AI\Platform\Event\ResultConvertedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
final class LogAiUsage
{
    public function __construct(private AiUsageRepository $usage, private AiFeatureContext $context) {}

    public function __invoke(ResultConvertedEvent $event): void
    {
        $tokens = $event->getResult()->getMetadata()->get('token_usage');

        $this->usage->record(
            feature: $this->context->current(),     // set by the caller, e.g. "support-bot"
            model: $event->getModel()->getName(),
            promptTokens: $tokens?->getPromptTokens() ?? 0,
            completionTokens: $tokens?->getCompletionTokens() ?? 0,
            cachedTokens: $tokens?->getCachedTokens() ?? 0,
        );
    }
}

Multiply by a price table you keep in config and you can answer "what does the support bot cost per tenant per month". Two caveats: for streamed responses the token counts arrive only after the stream is consumed, so record those from the stream's final metadata; and the web profiler panel shows the same data in development, but it is not a log.

Retries and background work with Messenger

Symfony AI calls are synchronous. Anything the user does not watch live should go through Messenger, as in the stable-components guide. The new part is mapping Symfony AI exceptions to Messenger retry behaviour:

use Symfony\AI\Platform\Exception\BadRequestException;
use Symfony\AI\Platform\Exception\RateLimitExceededException;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;

try {
    $summary = $this->agent->call($messages)->getContent();
} catch (RateLimitExceededException $e) {
    $delayMs = ($e->getRetryAfter() ?? 30) * 1000;
    throw new RecoverableMessageHandlingException($e->getMessage(), 0, $e, $delayMs);
} catch (BadRequestException $e) {
    throw new UnrecoverableMessageHandlingException($e->getMessage(), 0, $e);
}

The retry delay argument on RecoverableMessageHandlingException exists since Symfony 7.2, so it is available everywhere the bundle runs. Keep HttpClient retries low (one or two) and let Messenger handle longer waits; stacking three layers of retries turns one rate limit into minutes of blocked workers. For provider outages, the Failover platform bridge tries a list of platforms in order and can be wired from ai.yaml.

Rate limits per user and per tenant

Provider rate limits protect the provider, not your invoice. Put Symfony's RateLimiter in front of every agent call that a user can trigger, keyed by user and by tenant, exactly as shown in the stable-components guide. Also lower max_tool_calls: an agent that loops 50 times on one question is the most common cost spike we see.

Testing without calling the model

Symfony AI ships test doubles, and they are good:

  • InMemoryPlatform returns a fixed string or the result of a closure, so unit tests of your services never hit the network.
  • MockAgent returns scripted answers per input and records the calls for assertions.
  • AiAssertionsTrait for functional tests reads the profiler's AI collector: assertPlatformCallCount(), assertModelCalled(), assertToolCalled(). It needs the profiler enabled in the test environment.
$platform = new InMemoryPlatform(fn ($model, $input) => 'Order A-10423 ships tomorrow.');
$agent = new Agent($platform, 'claude-sonnet-5');

These test your wiring, not answer quality. Keep 50 to 200 real inputs with expected outputs and run them against the real model on every prompt or model change.

Upgrades

Plan a short upgrade task for every minor release you take: read the changelog, run the test suite and the evaluation set, and check your ai.yaml against the new config reference. In our experience that is typically 2 to 8 hours per minor version for an app with a few agents, which is a typical range, not a promise.

When should you use plain HttpClient instead of Symfony AI?

Use the stable approach, a scoped HttpClient behind your own interface, when any of these is true:

Situation Better choice
One or two AI features on one provider Plain HttpClient + Messenger
Symfony older than 7.3 or PHP older than 8.2 Plain HttpClient, or upgrade first
Policy against experimental or 0.x dependencies Plain HttpClient
Several providers, model switching per feature Symfony AI Platform
Agents with many tools and processors Symfony AI Agent
Retrieval with a vector database you may change later Symfony AI Store
Your app must be callable from Claude or ChatGPT MCP Bundle

They mix well. A common shape in our projects: Symfony AI Platform and Agent for the assistant, plain HttpClient for one batch job that uses a provider feature the bridge does not expose yet, and the same LanguageModel interface in front of both so business code does not care which one runs. If Symfony AI changes in a way you cannot absorb, only the adapter changes.

What do we recommend for Symfony teams?

Our verdict as of October 2026: Symfony AI is good enough to build real features on, provided you treat it as a fast-moving dependency. Pin the minor version, keep your own interface between business code and the agent, log tokens through ResultConvertedEvent, run every non-interactive call through Messenger, limit tool calls, and test with InMemoryPlatform plus an evaluation set. For the first feature in an older app, the stable HttpClient approach is still the shorter path.

We have shipped AI on PHP and Symfony in our own product, AI Resume Master, which reached 50,000 monthly active users, built retrieval-based memory in the AI Grief Companion, and built large Symfony back ends such as the custom ERP for a US aircraft service company.

Next step

If you want a Symfony team to add an agent, a retrieval feature or an MCP server to your product, see our Symfony development services. AI features in existing products start from $10,000 with us, and a focused first feature can run as one AI Sprint: $10,000 fixed for 4 weeks. Contact us with the feature you have in mind and your Symfony version.

Case studies

Frequently asked questions

As of October 2026 the latest release is v0.14.1 and every Symfony AI component and bundle is marked experimental, which means it is outside Symfony's backward compatibility promise. Teams do run it in production, but they pin the minor version, read the changelog before each upgrade and keep their business code behind their own interfaces so a breaking change touches one adapter, not every feature.

Version 0.14 of the components requires PHP 8.2 or newer. The AI Bundle and MCP Bundle require Symfony 7.3 or newer, including 7.4 LTS and the 8.x line. Older applications need an upgrade first, or they can use the stable HttpClient and Messenger approach, which works on any supported Symfony version.

The AI Bundle wires the Platform, Agent, Store and Chat components into the container from config/packages/ai.yaml, so your app can call models, run agents and search vectors. The MCP Bundle uses the official MCP PHP SDK to make your Symfony app an MCP server that Claude, ChatGPT or Cursor can call, or an MCP client that uses tools from other servers.

Not on its own. The platforms use Symfony HttpClient, so you can pass a scoped client with retry_failed for 429 and 5xx responses, and a Failover platform can switch providers. Background execution, delayed retries after a rate limit and per-user limits are your job, usually with Messenger and the RateLimiter component.

Use Symfony AI when you need several providers, agents with many tools, a vector store abstraction or an MCP server, and your app is on Symfony 7.3 or newer. Use a small HttpClient wrapper when you have one or two features on one provider, an older Symfony version or strict rules against experimental dependencies. Both can live in one app.

With us, an AI feature added to an existing Symfony product starts from $10,000 and usually takes 4 to 8 weeks, with a fixed quote after a short scoping call. A focused first feature, such as one agent or one MCP server, can run as one AI Sprint: $10,000 fixed for 4 weeks. Model API usage is billed by the provider directly.

Let’s start your project
Book a call