sylius / ask-ai-plugin
AI chat panel inside the Sylius admin, driven by the Admin MCP tools with human approval of write actions.
Fund package maintenance!
Requires
- php: ^8.2
- league/commonmark: ^2.6
- psr/clock: ^1.0
- sylius/admin-mcp-server-plugin: ^0.2
- sylius/sylius: ^2.2
- sylius/telemetry: ^1.0
- symfony/ai-agent: ^1.0@beta
- symfony/ai-anthropic-platform: ^1.0@beta
- symfony/ai-bundle: ^1.0@beta
- symfony/ai-platform: ^1.0@beta
- symfony/clock: ^7.4 || ^8.1
- symfony/http-foundation: ^7.4 || ^8.1
- symfony/messenger: ^7.4 || ^8.1
- symfony/uid: ^7.4 || ^8.1
- symfony/ux-live-component: ^2.36 || ^3.0
- symfony/yaml: ^7.4 || ^8.1
- webmozart/assert: ^1.11
Requires (Dev)
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.2
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-webmozart-assert: ^2.0
- phpunit/phpunit: ^10.5
- sylius-labs/coding-standard: ^4.4
- sylius/sylius-mate-extension: ^0.11
- sylius/test-application: ^2.0.0@alpha
- symfony/ai-mate: ^1.0@beta
- symfony/ai-ollama-platform: ^1.0@beta
- symfony/ai-symfony-mate-extension: ^1.0@beta
- symfony/browser-kit: ^7.4 || ^8.1
- symfony/css-selector: ^7.4 || ^8.1
- symfony/debug-bundle: ^7.4 || ^8.1
- symfony/dotenv: ^7.4 || ^8.1
- symfony/http-client: ^7.4 || ^8.1
- symfony/intl: ^7.4 || ^8.1
- symfony/runtime: ^7.4 || ^8.1
- symfony/web-profiler-bundle: ^7.4 || ^8.1
- symfony/webpack-encore-bundle: ^2.2
Suggests
- symfony/ai-gemini-platform: To run the agent on the Gemini API
- symfony/ai-ollama-platform: To run the agent on a local Ollama
- symfony/ai-open-ai-platform: To run the agent on the OpenAI API
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-11-06 06:18:20 UTC
README
Sylius Ask AI Plugin
Ask AI panel inside the Sylius admin, working the store through the Admin MCP tools with human approval of every write.
About
Ask AI panel inside the Sylius admin. The assistant works the store through the
tools of sylius/admin-mcp-server-plugin
as the logged-in administrator; write actions are proposed and need human approval.
Status: proof of concept. Working today: chat panel (Live Component hooked into the admin layout), conversation/message persistence, asynchronous turns via Messenger, a real agent loop over the Admin MCP tools, and the write gate: every write the agent attempts is held as a proposed action with Approve/Reject in the panel.
Requirements
- Sylius 2.2+, PHP 8.2+, Symfony 7.4+.
sylius/admin-mcp-server-plugininstalled and configured: the assistant works the store through its tools and the Admin API it talks to.- A working Admin API: the agent calls it as the administrator who is chatting, so the Lexik JWT
keypair must exist (
php bin/console lexik:jwt:generate-keypair; a fresh Sylius-Standard does not ship one) and that administrator needsROLE_API_ACCESS(Step 6). - A Messenger worker: turns run asynchronously, never in the web request.
- A model: an Anthropic API key (the default,
ai.platform.anthropic) or any other Symfony AI platform (OpenAI, Gemini, Ollama, …). For development without paying for tokens, see Developing without an API bill.
Installation
There is no Flex recipe yet; the steps are manual.
Step 1 - Add the plugin via Composer
The plugin runs on Symfony AI, which has no stable release yet (symfony/ai-* are 1.0@beta), so
a project with the default minimum-stability: stable of Sylius-Standard refuses to install it.
Allow beta packages first; prefer-stable (already set in Sylius-Standard) keeps everything
else on stable versions:
composer config minimum-stability beta
composer config prefer-stable true
composer require sylius/ask-ai-plugin
The Admin MCP plugin comes with it as a dependency. If it was not installed before, follow
its own installation steps too (OAuth keys, SYLIUS_ADMIN_MCP_SERVER_* variables, bundle
registration, configuration and routes import).
With Symfony Flex active (the default in Sylius-Standard) two things happen during this command:
-
The
league/oauth2-server-bundlerecipe is applied and createsconfig/packages/league_oauth2_server.yamlandconfig/routes/league_oauth2_server.yaml, which conflict with the configuration the Admin MCP plugin ships. Replace both with empty stubs, as its README says:echo "# Managed by sylius/admin-mcp-server-plugin" > config/packages/league_oauth2_server.yaml echo "# Routes provided by sylius/admin-mcp-server-plugin" > config/routes/league_oauth2_server.yaml
-
Flex registers the plugin's bundle in
config/bundles.phpbut, as there is no recipe, does not import its configuration, so thecache:clearthat runs at the end ofcomposer requirefails withbus "sylius_ask_ai.command_bus" ... does not exist. This is expected: the error goes away after Step 3.
Step 2 - Register the bundle
Skip this step when Flex has already added the plugin to config/bundles.php. Otherwise add it
after the Admin MCP plugin:
return [ // ... Symfony\AI\AiBundle\AiBundle::class => ['all' => true], Symfony\AI\McpBundle\McpBundle::class => ['all' => true], Sylius\AdminMcpServerPlugin\SyliusAdminMcpServerPlugin::class => ['all' => true], Sylius\AskAiPlugin\SyliusAskAiPlugin::class => ['all' => true], ];
AiBundle (symfony/ai-bundle, installed with the plugin) builds the platform the agent talks
to; the plugin configures its Anthropic platform with ANTHROPIC_API_KEY, so nothing else is
needed to start.
Step 3 - Import the configuration
# config/packages/sylius_ask_ai.yaml imports: - { resource: "@SyliusAskAiPlugin/config/config.yaml" }
This registers the resources, the Messenger bus and transport (sylius_ask_ai, on
Doctrine by default; override with SYLIUS_ASK_AI_TRANSPORT_DSN) and the twig hook
that renders the panel in the admin layout.
The panel's styles and behaviour ship in assets/admin/entrypoint.js. Import it from your admin
entrypoint and rebuild the admin assets:
// assets/admin/entrypoint.js import '@vendor/sylius/ask-ai-plugin/assets/admin/entrypoint';
(@vendor is the alias Sylius-Standard's Webpack configuration defines; a relative
../../vendor/sylius/ask-ai-plugin/assets/admin/entrypoint.js works too.)
yarn encore prod
Then import the routes of the admin log (read-only pages listing conversations and agent actions):
# config/routes/sylius_ask_ai.yaml sylius_ask_ai: resource: "@SyliusAskAiPlugin/config/routes.yaml"
Finally clear the cache, twice:
php bin/console cache:clear php bin/console cache:clear
The first run rebuilds the container while booting and, as Symfony then considers the cache
fresh, only re-runs the optional warmers in place: the translation catalogues built before the
plugin was installed stay, and the panel shows raw sylius_ask_ai.ui.* keys. The second run
finds a fresh container, warms up from scratch and picks up the plugin's translations.
(Removing var/cache before a single cache:clear works as well.)
Step 4 - Run database migrations
php bin/console doctrine:migrations:migrate -n
Step 5 - Run the worker
php bin/console messenger:consume sylius_ask_ai
In production keep it under a process manager (Supervisor, systemd) like any other Messenger
worker. The worker is where the model is called: on the default platform (the Anthropic API) it
needs ANTHROPIC_API_KEY in its environment.
Step 6 - Give administrators API access
The agent calls the Admin API as the administrator who is chatting, so that administrator
needs ROLE_API_ACCESS. The panel then appears on every admin page, for the administrators
with that role only (sylius_ask_ai.access.role names another role when the assistant
should be narrower than the API, see Configuration).
Configuration
Everything lives under sylius_ask_ai (bin/console config:dump-reference sylius_ask_ai prints the full tree):
sylius_ask_ai: tools: # which Admin MCP tools the agent gets, by name prefix read_only_prefixes: [list_, get_, search_, count_, open_] write_prefixes: [create_, update_, add_, remove_, set_, archive_, restore_, resend_, cancel_, ship_, complete_, accept_, reject_, generate_] exclude: [] # single tools taken away by name, e.g. [update_channel] max_result_size: 50000 # bytes; a longer tool result is cut and the model told to narrow the query access: role: ROLE_API_ACCESS # who sees and uses the panel; a role of your own must imply ROLE_API_ACCESS prompt: additional_instructions: ~ # rules of the store appended to the system prompt, see The agent time_zone: ~ # what "today" means to the agent, e.g. Europe/Warsaw; default: the worker's PHP time zone poll_interval: 2000 # ms, how often the panel polls during a turn max_output_tokens: 8192 # the output budget of one model answer; a proposed write carries the whole resource suggestions: [ ... ] # the questions offered on an empty conversation, see The chat panel navigation: pages: { ... } # the admin pages the agent may open, see Navigation resources: # Sylius resources: conversation, message, agent_action conversation: classes: model: App\Entity\Conversation
The agent
The agent is ai.agent.sylius_ask_ai, defined by symfony/ai-bundle from
configuration the plugin prepends; the plugin only adds the Admin MCP tools to its toolbox, cuts
every tool result down to tools.max_result_size bytes (a page of products from the Admin API is
over 400 KB, enough of them end a turn with "prompt is too long") and tags its processors for it. So the agent is configured where every Symfony AI agent is, and the
application overrides any of it:
# config/packages/ai.yaml ai: agent: sylius_ask_ai: platform: ai.platform.anthropic # any PlatformInterface service id model: 'claude-haiku-4-5?max_output_tokens=8192' # the default model and its output budget (sylius_ask_ai.max_output_tokens) prompt: file: '%kernel.project_dir%/config/ai/system_prompt.txt' max_tool_calls: 50 message_store: sylius_ask_ai.chat.message_store # the conversation entities; the reply keeps the tools it called fault_tolerant_toolbox: false
The system prompt is config/prompts/system_prompt.txt, a
Symfony AI string template: {now} is rendered with today's date (weekday and time zone
included, so "today" and "this week" mean something; prompt.time_zone picks the zone when the
worker's PHP does not run in the store's), {pages} with the admin pages the agent may open and
{store_instructions} with what the store adds (CurrentDateInputProcessor,
AdminPagesInputProcessor and StoreInstructionsInputProcessor set the template variables).
The file is sent to the model as it is, so it has no comments. A few rules of your own go in
without touching the file:
sylius_ask_ai: prompt: additional_instructions: | Always answer in Polish. Our SKUs look like FW-1234; a "collection" is a taxon under /collections.
To rewrite the prompt, copy the file into your application, edit the copy and point
prompt.file at it; the rules about open_admin_page and awaiting_approval are what the chat
panel relies on, keep them unless you change the panel too. The other prompt options of the
bundle (include_tools, translation) work as documented in the
AI bundle.
The admin log
The "Ask AI" section of the admin menu lists every conversation (/admin/ask-ai/conversations,
with the first question as its topic, the administrator, state and dates) and every action the agent
proposed (/admin/ask-ai/agent-actions: tool, request, state, who decided and when). A conversation's
page replays it with the same bubbles and action cards as the panel, read-only; an action's page shows
the tool arguments, request body, the snapshot taken before the change and the result. Both grids are
Sylius grids (sylius_ask_ai_admin_conversation, sylius_ask_ai_admin_agent_action) and
can be extended the usual way; the pages are built from twig hooks under sylius_ask_ai.conversation.show
and sylius_ask_ai.agent_action.show.
How a turn runs
The agent is built on the Symfony AI components. symfony/ai-agent runs the turn: the model
of your choice through symfony/ai-platform, with the Admin MCP tools as its toolbox,
called in-process (the same services the MCP server exposes; delete_* and refund_* are
never exposed). The agent continues and saves the conversation on its message store, backed by
the Conversation/Message entities (a conversation is stored under its uuid). The tools the assistant called are stored with its reply
and shown under it in the panel.
Choosing the model
ai.platform.anthropic (the default) works without installing anything:
-
The Anthropic API, built by
symfony/ai-bundle. SetANTHROPIC_API_KEYwhere the worker runs and it works; the default model isclaude-haiku-4-5.modeltakes any name from the Anthropic bridge's model catalog (symfony/ai-anthropic-platform); a model newer than the installed bridge needs a bridge update. The plugin only prepends the key; everything else about the platform is the bundle's own configuration, override it underai.platform.anthropic(http_client,cache_retention, …):# config/packages/ai.yaml ai: platform: anthropic: api_key: '%env(ANTHROPIC_API_KEY)%' http_client: app.ai_http_client cache_retention: long
A model of your own names its output budget itself, in the provider's own option (the
examples below): a proposed write carries the whole resource, and the 1000 tokens Claude
defaults to cut it in the middle of the tool call, which fails the turn. sylius_ask_ai.max_output_tokens
sets the budget of the default model only.
Any other Symfony AI platform works the same way once the vendor's platform package is installed. For OpenAI:
composer require symfony/ai-open-ai-platform
# config/packages/ai.yaml ai: platform: openai: api_key: '%env(OPENAI_API_KEY)%' agent: sylius_ask_ai: platform: ai.platform.openai model: 'gpt-4o-mini?max_completion_tokens=8192'
| Vendor | Package | ai.platform key |
platform |
|---|---|---|---|
| OpenAI | symfony/ai-open-ai-platform |
openai |
ai.platform.openai |
| Gemini | symfony/ai-gemini-platform |
gemini |
ai.platform.gemini |
| Ollama | symfony/ai-ollama-platform |
ollama |
ai.platform.ollama |
The symfony/ai-bundle recipe has already created config/packages/ai.yaml with an ai: key
(and the vendor's platform recipe adds its ai.platform.<vendor> entry and <VENDOR>_API_KEY
there): put the agent part under that existing key rather than pasting the snippet at the end
of the file, which gives Duplicate key "ai".
Ollama takes endpoint instead of api_key. The full list of platforms and their options is in
the Symfony AI documentation; any other service
implementing PlatformInterface works too, point platform at its id.
Developing without an API bill
The functional tests run on a scripted platform and cost nothing. For clicking through the panel
during development, two platforms with tool calling cost nothing either; both are the same agent
on a different platform, nothing in the plugin changes:
-
Ollama, a local model. Install Ollama, pull a model that supports tools (
ollama pull qwen3), then:composer require symfony/ai-ollama-platform
# config/packages/dev/ai.yaml ai: platform: ollama: endpoint: 'http://127.0.0.1:11434' agent: sylius_ask_ai: platform: ai.platform.ollama model: 'qwen3?num_ctx=32768&num_predict=8192'
For the plugin itself,
tests/TestApplication/config/ollama.yamldoes this for the test application: add it toSYLIUS_TEST_APP_CONFIGS_TO_IMPORTintests/TestApplication/.env.local.num_ctxmatters: Ollama defaults to a 4096-token window and silently drops the front of a longer prompt, and the tool definitions alone are ~22k tokens, so without it the model sees only the tail of the tool list and claims the tools do not exist.Local models follow the system prompt less reliably than Claude: expect the simple flows (lists, navigation, a single proposal) to work and long tool chains to be hit and miss. A turn also costs real time: on a 16 GB laptop a 4B model reads the cold prompt for over a minute and thinks for another before every tool call, so a machine with more memory bandwidth (a Max-class Apple chip or a discrete GPU) is what makes Ollama comfortable.
-
Gemini on the free tier of Google AI Studio: a free API key with a per-minute quota.
composer require symfony/ai-gemini-platform
# config/packages/dev/ai.yaml ai: platform: gemini: api_key: '%env(GEMINI_API_KEY)%' agent: sylius_ask_ai: platform: ai.platform.gemini model: 'gemini-2.5-flash?max_output_tokens=8192'
Customization
Everything below is either configuration or the usual Symfony and Sylius extension points; no fork and no copy of a service definition is needed for:
| To | Use |
|---|---|
| Another vendor or model | ai.agent.sylius_ask_ai.platform / model (Choosing the model) |
| A few rules of the store in the prompt | sylius_ask_ai.prompt.additional_instructions (The agent) |
| A prompt of your own | ai.agent.sylius_ask_ai.prompt.file |
| Fewer tools, or one tool less | tools.read_only_prefixes, tools.write_prefixes, tools.exclude (The write gate) |
| A tool of your own | #[AsTool] + the sylius_ask_ai.tool tag, or tools: in ai.yaml (Your own tools) |
| A resource of your own the agent may open | sylius_ask_ai.navigation.pages (Navigation) |
| Who sees and uses the panel | sylius_ask_ai.access.role |
| The store's own suggested questions | sylius_ask_ai.suggestions (The chat panel) |
| Something added to, removed from or reordered in the panel | the sylius_ask_ai.chat_panel.* twig hooks (The chat panel) |
| Another look for a bubble, the action card, the log pages | templates/bundles/SyliusAskAiPlugin/… (Models and services) |
| Extra fields on the entities, another repository | sylius_ask_ai.resources.<name>.classes (Models and services) |
| Different behaviour of a service | Decorate it by id or interface (Models and services) |
Models and services
The entities (Conversation, Message, AgentAction) are Sylius resources: each has an
interface, is mapped in config/doctrine as a mapped superclass, and can be replaced under
sylius_ask_ai.resources.<name>.classes.model; the resource bundle provides
sylius_ask_ai.repository.<name> and sylius_ask_ai.factory.<name>; the message
and agent action factories are decorated with createForConversation() and createProposal(), the only
ways these two are created (MessageFactoryInterface, AgentActionFactoryInterface). Every
service has an id (sylius_ask_ai.*) and an interface aliased to it, so a service can be
decorated or swapped by id. A turn that fails for any reason leaves the conversation in the
failed state with a notice in the panel; a turn that never comes back can be cancelled there.
Templates live under the bundle's @SyliusAskAiPlugin namespace, so the application
overrides any of them the Symfony way: copy the file to
templates/bundles/SyliusAskAiPlugin/<same path> (for example
templates/bundles/SyliusAskAiPlugin/component/action_card.html.twig for the proposed
action card, component/message.html.twig for a bubble). The panel itself is put together
from twig hooks, so most changes to it need no copy at all.
The chat panel
The open panel renders the sylius_ask_ai.chat_panel hook with the live component as
panel in the context (hookable_metadata.context.panel: conversation, timeline,
running). Its hookables are themselves hooks, with the templates under
templates/component/chat_panel/:
| Hook | Hookables (priority) |
|---|---|
sylius_ask_ai.chat_panel |
header (200), body (100), footer (0) |
sylius_ask_ai.chat_panel.header |
title (200), history (100), new_chat (0) |
sylius_ask_ai.chat_panel.body |
empty_state (100), timeline (0); the context also carries timeline |
sylius_ask_ai.chat_panel.body.empty_state |
intro (100), suggestions (0) |
sylius_ask_ai.chat_panel.footer |
composer (0) |
The suggested questions on an empty conversation are a list in the configuration, each a translation key or plain text; the list replaces the default one, so give the whole set, in order, and an empty list shows none:
sylius_ask_ai: suggestions: - app.ask_ai.suggestion.unpaid_orders # translated in your messages.<locale>.yaml - Which coupons expire this month? # or plain text, as is (a literal % is written %%) - sylius_ask_ai.ui.suggestion.create_a_customer_group_called_vip # one of the defaults kept
For anything else, the usual sylius_twig_hooks configuration adds, replaces, reorders or
disables a part, for example a notice above the composer or suggestions rendered your own way:
sylius_twig_hooks: hooks: 'sylius_ask_ai.chat_panel.footer': notice: template: 'admin/ask_ai/notice.html.twig' priority: 10 'sylius_ask_ai.chat_panel.body.empty_state': suggestions: template: 'admin/ask_ai/suggestions.html.twig'
A suggestion is any element with data-ask-ai-suggestion="<the question>": clicking it
sends the question; the configured list is panel.suggestions in the hook context.
The composer's textarea keeps the ask-ai__input class and the draft live model, which
the keyboard handling and the browser tests rely on.
The write gate
ProposingApiClient decorates the Admin MCP API client. Inside a chat turn, POST/PUT/PATCH/ DELETE are not sent: an AgentAction (tool name, arguments, method, path, body, snapshot of
the current resource) is stored as proposed and the tool returns an awaiting_approval
notice the model relays. Approving in the panel dispatches ExecuteAgentAction: the worker
replays the call as the approving administrator, stores the result, and runs a follow-up
turn so the assistant reports the outcome. Rejections are passed to the model with the next
message. Reads always pass through, as does everything outside a chat turn (OAuth clients).
Requirements: the administrator using the chat needs ROLE_API_ACCESS (the agent calls the
Admin API as them). Which tools the agent gets: sylius_ask_ai.tools.read_only_prefixes
and tools.write_prefixes, minus the names listed in tools.exclude.
Your own tools
A tool of your own is a Symfony AI tool (#[AsTool], or a service with a name, a description
and a method) that joins the Admin MCP tools in the agent's toolbox; it is not served by the
MCP server, and the write gate does not apply to it (it is called in-process, not through the
Admin API). Either tag it for this agent only:
#[AsTool(name: 'count_open_tickets', description: 'count_open_tickets() → the number of open support tickets.')] final class CountOpenTickets { public function __invoke(): int { /* ... */ } }
services: App\Ai\Tool\CountOpenTickets: tags: [sylius_ask_ai.tool]
or list it under the agent in ai.yaml, as for any Symfony AI agent (tools: [{ service: ... }],
with name, description and method when the class has no #[AsTool]); the Admin MCP
tools are added to that list. Tools tagged ai.tool for every agent of the application join as
well. The plugin's own open_admin_page is registered this way.
Navigation
Showing things means opening the page, not pasting data into the chat. The agent's own tool
open_admin_page(resource, identifier?, page?, search?) opens the show page of one resource
("open the most expensive product"), its edit page ("edit product X"), or the list page,
filtered through the grid's search when a term is given ("show me the products with 'mug' in
the name", "show me the customer groups"). The system prompt tells the model to open pages
for see/show/open/edit requests and to answer with one confirming sentence, and lists the
pages it can open; data goes to the chat only for questions. After an approved action is
executed, the panel goes to the page of what was created (show page) or changed (edit page)
on its own, from the @id of the API response; nothing happens when the administrator is
already there. The pages the agent may open are sylius_ask_ai.navigation.pages, a
whitelist keyed by the Admin API collection segment (the defaults are in the plugin's
config/config.yaml); the panel performs the navigation on its next poll and keeps itself
open across the page load (cookie). Add a resource of your own, or switch a default one off:
sylius_ask_ai: navigation: pages: tickets: # /api/v2/admin/tickets/{id} repository: app.repository.ticket # where the identifier is looked up field: id # the field the Admin API identifies it by show: app_admin_ticket_show # optional, "show" falls back to edit edit: app_admin_ticket_update index: app_admin_ticket_index # optional, no list page without it search: search # optional, the grid filter a search term goes to # parent: { param: customerId, path: customer.id } for a nested resource exchange-rates: false
Development
composer install vendor/bin/console doctrine:database:create vendor/bin/console doctrine:migrations:migrate -n vendor/bin/console sylius:fixtures:load default -n (cd vendor/sylius/test-application && yarn install && yarn build) symfony server:start -d --document-root=vendor/sylius/test-application/public --no-tls vendor/bin/console messenger:consume sylius_ask_ai --sleep=0.25 -vv
Admin: http://127.0.0.1:8000/admin/ (sylius / sylius). Database and Admin MCP plugin
settings live in tests/TestApplication/.env (override in .env.local).
Browser click-through of the panel (Playwright, against the running app): tests/e2e.
Dev tooling: Symfony AI Mate with the Sylius extension is installed (vendor/bin/mate);
it boots Sylius\TestApplication\Kernel from tests/TestApplication/.env.
Contributing
Would like to help us build a more capable Sylius admin? Start from reading our Contribution Guide.
Bug Tracking
If you want to report a bug or suggest an idea, please use GitHub issues.
Security Issues
If you think that you have found a security issue, please do not use the issue tracker and do
not post it publicly. Instead, all security issues must be sent to security@sylius.com.
Community Support
Get Sylius support on Slack, Forum or Stack Overflow.
Telemetry
The plugin registers the sylius/telemetry compiler pass, which keeps Sylius telemetry running
in production for as long as the plugin is installed. Telemetry is a condition of the free
production use under the license; what it sends and how to stay compliant is in the
License FAQ.
License
Released under the Business Source License 1.1. The plugin is source-available, not open source, and each version converts to the MIT License four years after it is published.
Production use is free as long as Sylius telemetry is not deliberately disabled and the plugin is
not used to provide a competing offering. Setting any of the required telemetry settings to 0
(e.g. SYLIUS_TELEMETRY_BUSINESS=0) in production breaches this condition: you need a commercial
license. See the
License FAQ and the Licensor Statement.
For commercial licensing, contact contact@sylius.com.