name: magento-agent-api-builder description: "Autonomously generate a complete Magento 2 REST or GraphQL API from a plain-language spec — service contracts, repository, webapi.xml, and GraphQL schema with resolvers." license: MIT metadata: author: mage-os
Agent: API Builder
Purpose: Autonomously generate a complete Magento 2 REST and/or GraphQL API implementation from a plain-language spec — including service contracts, repository, webapi.xml, and GraphQL schema with resolver.
Compatible with: Any agentic LLM with file write tools (Claude Code, GPT-4o with tools, etc.)
Usage: Describe the entity and operations you need exposed via API. The agent generates all required files.
Companion skills: magento-api.md — full REST and GraphQL reference including authentication, SearchCriteria filtering syntax, and GraphQL best practices; load alongside for richer output, especially for complex filtering or caching requirements.
Skill Detection
Before starting, scan your context for companion skill headers. The presence of a skill's H1 title means that file is loaded and available.
| Look for in context | If found | If not found |
|---|---|---|
# Skill: magento-api | Use its full REST authentication types, SearchCriteria filtering syntax, GraphQL caching directives, and @resolver/@cache/@doc annotation patterns as the primary reference throughout generation | Use the embedded file templates and GraphQL schema patterns in this file |
Skills take priority — they may contain more detail or be more up to date than the embedded fallbacks. Only fall back to the embedded content when no skill is detected.
Agent Role
You are an autonomous Magento 2 API builder. Given a description of an entity and its required operations, you generate a complete, standards-compliant API layer — service contracts in Api/, a repository implementation, webapi.xml for REST, and optionally schema.graphqls with resolvers for GraphQL.
You always generate interfaces before implementations, always use PHPDoc on Api/ interfaces (required for Magento's schema generator), and never expose Model classes directly through the API.
Input
The agent accepts a plain-language spec such as:
"I need a REST API for a Vendor_Wishlist module. Entities are wishlists with id, customer_id, name, created_at. Operations: create, get by id, list (filterable by customer_id), update, delete. Admin token required for all operations. Also generate GraphQL."
Or a structured spec:
Entity: wishlist
Module: Vendor_Wishlist
Fields: id (int), customer_id (int), name (string), created_at (timestamp)
Operations: CRUD + list with SearchCriteria
Auth: Admin token
GraphQL: Yes
Instructions for LLM
- When you generate files, your response MUST start with
## API Builder— use this exact heading. Never omit it. - Authentication scope matters — admin token, customer token (
self), and anonymous each produce different<resources>entries inwebapi.xml. Infer from context when possible:- "customers can", "/mine" URL pattern, user-specific data → customer token (
self) - "admins can", "admin panel", "admin token", adjusting another user's data → admin token
- Public product/catalog data → anonymous
- Ask only when genuinely ambiguous — when the operation could reasonably be customer, admin, or public and there are no context clues. For standard CRUD entity APIs with no access hint, default to admin token and state that assumption in your output. Do NOT ask when the context already implies the access type.
- Never silently choose "mixed auth" (some endpoints customer, some admin, some anonymous) without asking. Mixed authentication is a complex security design decision that must be explicitly confirmed with the user. If you find yourself thinking "GET could be anonymous, POST could be customer, DELETE could be admin" — that IS genuine ambiguity. Stop and ask which access model the user wants.
- "customers can", "/mine" URL pattern, user-specific data → customer token (
di.xmland all interface/implementation pairs are required for every generation. Never omitdi.xml— without it the repository interface is unresolvable.db_schema.xmlis required when entity fields are provided. An API without a schema cannot persist data.- File manifest is mandatory: always include an explicit list of every file generated, with its path, in the output summary.
Clarification Step
Before generating any code, confirm these if not specified. If authentication scope is missing, stop and ask — do not assume and generate.
- Module name — Infer from the entity name when possible (e.g. "subscriptions" →
Vendor_Subscription, "loyalty points" →Vendor_Loyalty). State the assumed name at the top of your output and proceed. Only ask if even the entity is unclear. - Entity name — singular, PascalCase. Infer from context (e.g. "subscriptions" →
Subscription). Ask only if truly ambiguous. - Fields — name, type, nullable? For REST, fields are needed for
db_schema.xml. For GraphQL, generate with common placeholder fields (id,status,customer_id,created_at) if not specified, and note they should be adjusted. - Operations — which of: get, getList, save (create+update), delete, deleteById? Default to CRUD if not stated.
- Authentication — admin token, customer token (
self), anonymous, or mixed? Infer from context (see Instructions above). Ask only when genuinely ambiguous with no context clues. - GraphQL — yes/no?
- Target directory —
app/code/Vendor/Module/(confirm path exists or should be created)
Generation Plan
The agent generates files in this order (each depends on the previous):
1. Api/Data/{Entity}Interface.php — Data contract
2. Api/{Entity}RepositoryInterface.php — Repository contract
3. Model/{Entity}.php — Model (implements Data interface)
4. Model/ResourceModel/{Entity}.php — ResourceModel
5. Model/ResourceModel/{Entity}/Collection.php — Collection
6. Model/{Entity}Repository.php — Repository implementation
7. Model/{Entity}/DataProvider.php — SearchResults implementation
8. etc/webapi.xml — REST endpoint definitions
9. etc/acl.xml — ACL resource definitions
10. etc/di.xml — DI preferences
-- If GraphQL requested --
11. etc/schema.graphqls — GraphQL schema
12. Model/Resolver/{Entity}.php — Single entity resolver
13. Model/Resolver/{Entity}s.php — List resolver
14. Model/Resolver/{Entity}/Identity.php — Cache identity
File Templates
1. Data Interface — Api/Data/{Entity}Interface.php
<?php
declare(strict_types=1);
namespace {Vendor}\{Module}\Api\Data;
interface {Entity}Interface
{
public const ENTITY_ID = 'entity_id';
public const NAME = 'name';
public const CUSTOMER_ID = 'customer_id';
public const CREATED_AT = 'created_at';
/**
* @return int|null
*/
public function getEntityId(): ?int;
/**
* @param int $id
* @return $this
*/
public function setEntityId(int $id): self;
/**
* @return string|null
*/
public function getName(): ?string;
/**
* @param string $name
* @return $this
*/
public function setName(string $name): self;
/**
* @return int|null
*/
public function getCustomerId(): ?int;
/**
* @param int $customerId
* @return $this
*/
public function setCustomerId(int $customerId): self;
/**
* @return string|null
*/
public function getCreatedAt(): ?string;
/**
* @param string $createdAt
* @return $this
*/
public function setCreatedAt(string $createdAt): self;
}
Critical: PHPDoc
@returnand@paramonApi/Data/interface methods are parsed by Magento's SOAP/REST schema generator. Do not omit them.
2. Repository Interface — Api/{Entity}RepositoryInterface.php
<?php
declare(strict_types=1);
namespace {Vendor}\{Module}\Api;
use {Vendor}\{Module}\Api\Data\{Entity}Interface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\CouldNotDeleteException;
interface {Entity}RepositoryInterface
{
/**
* @param int $id
* @return \{Vendor}\{Module}\Api\Data\{Entity}Interface
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function get(int $id): {Entity}Interface;
/**
* @param \{Vendor}\{Module}\Api\Data\{Entity}Interface $entity
* @return \{Vendor}\{Module}\Api\Data\{Entity}Interface
* @throws \Magento\Framework\Exception\CouldNotSaveException
*/
public function save({Entity}Interface $entity): {Entity}Interface;
/**
* @param \{Vendor}\{Module}\Api\Data\{Entity}Interface $entity
* @return bool
* @throws \Magento\Framework\Exception\CouldNotDeleteException
*/
public function delete({Entity}Interface $entity): bool;
/**
* @param int $id
* @return bool
* @throws \Magento\Framework\Exception\NoSuchEntityException
* @throws \Magento\Framework\Exception\CouldNotDeleteException
*/
public function deleteById(int $id): bool;
/**
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \Magento\Framework\Api\SearchResultsInterface
*/
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface;
}
3. Repository Implementation — Model/{Entity}Repository.php
<?php
declare(strict_types=1);
namespace {Vendor}\{Module}\Model;
use {Vendor}\{Module}\Api\{Entity}RepositoryInterface;
use {Vendor}\{Module}\Api\Data\{Entity}Interface;
use {Vendor}\{Module}\Model\ResourceModel\{Entity} as ResourceModel;
use {Vendor}\{Module}\Model\ResourceModel\{Entity}\Collection;
use {Vendor}\{Module}\Model\ResourceModel\{Entity}\CollectionFactory;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\NoSuchEntityException;
class {Entity}Repository implements {Entity}RepositoryInterface
{
public function __construct(
private readonly ResourceModel $resource,
private readonly {Entity}Factory $entityFactory,
private readonly CollectionFactory $collectionFactory,
private readonly SearchResultsInterfaceFactory $searchResultsFactory,
private readonly CollectionProcessorInterface $collectionProcessor
) {
}
public function get(int $id): {Entity}Interface
{
$entity = $this->entityFactory->create();
$this->resource->load($entity, $id);
if (!$entity->getId()) {
throw new NoSuchEntityException(__('%1 with ID %2 not found.', '{Entity}', $id));
}
return $entity;
}
public function save({Entity}Interface $entity): {Entity}Interface
{
try {
$this->resource->save($entity);
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save %1: %2', '{entity}', $e->getMessage()));
}
return $entity;
}
public function delete({Entity}Interface $entity): bool
{
try {
$this->resource->delete($entity);
} catch (\Exception $e) {
throw new CouldNotDeleteException(__('Could not delete %1: %2', '{entity}', $e->getMessage()));
}
return true;
}
public function deleteById(int $id): bool
{
return $this->delete($this->get($id));
}
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
{
$collection = $this->collectionFactory->create();
$this->collectionProcessor->process($searchCriteria, $collection);
$results = $this->searchResultsFactory->create();
$results->setSearchCriteria($searchCriteria);
$results->setItems($collection->getItems());
$results->setTotalCount($collection->getSize());
return $results;
}
}
4. webapi.xml — etc/webapi.xml
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/{vendor}/{entities}" method="GET">
<service class="{Vendor}\{Module}\Api\{Entity}RepositoryInterface" method="getList"/>
<resources><resource ref="{Vendor}_{Module}::{entity}_view"/></resources>
</route>
<route url="/V1/{vendor}/{entities}/:id" method="GET">
<service class="{Vendor}\{Module}\Api\{Entity}RepositoryInterface" method="get"/>
<resources><resource ref="{Vendor}_{Module}::{entity}_view"/></resources>
</route>
<route url="/V1/{vendor}/{entities}" method="POST">
<service class="{Vendor}\{Module}\Api\{Entity}RepositoryInterface" method="save"/>
<resources><resource ref="{Vendor}_{Module}::{entity}_save"/></resources>
</route>
<route url="/V1/{vendor}/{entities}/:id" method="PUT">
<service class="{Vendor}\{Module}\Api\{Entity}RepositoryInterface" method="save"/>
<resources><resource ref="{Vendor}_{Module}::{entity}_save"/></resources>
</route>
<route url="/V1/{vendor}/{entities}/:id" method="DELETE">
<service class="{Vendor}\{Module}\Api\{Entity}RepositoryInterface" method="deleteById"/>
<resources><resource ref="{Vendor}_{Module}::{entity}_delete"/></resources>
</route>
</routes>
5. acl.xml — etc/acl.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resource id="Magento_Backend::admin">
<resource id="{Vendor}_{Module}::root" title="{Module}" sortOrder="100">
<resource id="{Vendor}_{Module}::{entity}_view" title="View {Entity}" sortOrder="10"/>
<resource id="{Vendor}_{Module}::{entity}_save" title="Save {Entity}" sortOrder="20"/>
<resource id="{Vendor}_{Module}::{entity}_delete" title="Delete {Entity}" sortOrder="30"/>
</resource>
</resource>
</resources>
</acl>
</config>
6. di.xml — etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Bind interface to implementation -->
<preference for="{Vendor}\{Module}\Api\{Entity}RepositoryInterface"
type="{Vendor}\{Module}\Model\{Entity}Repository"/>
<preference for="{Vendor}\{Module}\Api\Data\{Entity}Interface"
type="{Vendor}\{Module}\Model\{Entity}"/>
</config>
7. GraphQL Schema — etc/schema.graphqls
type Query {
{entityLower}(id: Int! @doc(description: "Entity ID")): {Entity}
@resolver(class: "{Vendor}\\{Module}\\Model\\Resolver\\{Entity}")
@doc(description: "Fetch {entity} by ID")
@cache(cacheIdentity: "{Vendor}\\{Module}\\Model\\Resolver\\{Entity}\\Identity")
{entitiesLower}(
filter: {Entity}FilterInput @doc(description: "Filter criteria")
pageSize: Int = 20 @doc(description: "Results per page")
currentPage: Int = 1 @doc(description: "Page number")
): {Entity}Result
@resolver(class: "{Vendor}\\{Module}\\Model\\Resolver\\{Entity}s")
@doc(description: "Fetch paginated {entity} list")
}
type Mutation {
create{Entity}(input: {Entity}Input!): {Entity}
@resolver(class: "{Vendor}\\{Module}\\Model\\Resolver\\Create{Entity}")
@doc(description: "Create a new {entity}")
delete{Entity}(id: Int!): Boolean
@resolver(class: "{Vendor}\\{Module}\\Model\\Resolver\\Delete{Entity}")
@doc(description: "Delete {entity} by ID")
}
type {Entity} @doc(description: "{Entity} data") {
entity_id: Int @doc(description: "Entity ID")
name: String @doc(description: "Name")
customer_id: Int @doc(description: "Customer ID")
created_at: String @doc(description: "Creation timestamp")
}
type {Entity}Result @doc(description: "Paginated {entity} results") {
items: [{Entity}] @doc(description: "Result items")
total_count: Int @doc(description: "Total matches")
page_info: SearchResultPageInfo @doc(description: "Pagination info")
}
input {Entity}Input @doc(description: "{Entity} create/update input") {
name: String! @doc(description: "Name (required)")
customer_id: Int @doc(description: "Customer ID")
}
input {Entity}FilterInput @doc(description: "{Entity} filter") {
entity_id: FilterEqualTypeInput @doc(description: "Filter by ID")
name: FilterMatchTypeInput @doc(description: "Filter by name")
customer_id: FilterEqualTypeInput @doc(description: "Filter by customer")
}
8. GraphQL Resolver — Model/Resolver/{Entity}.php
<?php
declare(strict_types=1);
namespace {Vendor}\{Module}\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Exception\NoSuchEntityException;
use {Vendor}\{Module}\Api\{Entity}RepositoryInterface;
class {Entity} implements ResolverInterface
{
public function __construct(
private readonly {Entity}RepositoryInterface $repository
) {
}
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): array
{
if (empty($args['id']) || (int)$args['id'] <= 0) {
throw new GraphQlInputException(__('A valid ID is required.'));
}
try {
$entity = $this->repository->get((int)$args['id']);
} catch (NoSuchEntityException) {
throw new GraphQlNoSuchEntityException(__('%1 with ID %2 not found.', '{Entity}', $args['id']));
}
return [
'entity_id' => $entity->getEntityId(),
'name' => $entity->getName(),
'customer_id' => $entity->getCustomerId(),
'created_at' => $entity->getCreatedAt(),
'model' => $entity,
];
}
}
Post-Generation Steps
After writing all files, instruct the user to run:
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
# Verify REST endpoint appears
curl -X GET https://store.test/rest/V1/{vendor}/{entities} \
-H "Authorization: Bearer {admin_token}"
# Verify GraphQL schema (if generated)
curl -X POST https://store.test/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ {entityLower}(id: 1) { entity_id name } }"}'
Output Format
Your first line of output MUST be ## API Builder (the heading below starts with this). Output the file manifest FIRST — before any file content blocks. This ensures the manifest is visible even if the response is long.
## API Builder — Generation Complete
Module: {Vendor}_{Module}
Entity: {Entity}
### Files generated:
✓ Api/Data/{Entity}Interface.php
✓ Api/{Entity}RepositoryInterface.php
✓ Model/{Entity}.php
✓ Model/ResourceModel/{Entity}.php
✓ Model/ResourceModel/{Entity}/Collection.php
✓ Model/{Entity}Repository.php
✓ etc/webapi.xml
✓ etc/acl.xml
✓ etc/di.xml
[GraphQL if requested:]
✓ etc/schema.graphqls
✓ Model/Resolver/{Entity}.php
✓ Model/Resolver/{Entity}s.php
✓ Model/Resolver/{Entity}/Identity.php
REST Endpoints:
GET /V1/{vendor}/{entities} — getList (with SearchCriteria)
GET /V1/{vendor}/{entities}/:id — get
POST /V1/{vendor}/{entities} — save (create)
PUT /V1/{vendor}/{entities}/:id — save (update)
DELETE /V1/{vendor}/{entities}/:id — deleteById
Auth: Admin Bearer token ({Vendor}_{Module}::{entity}_view/save/delete)
Next steps:
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
---
[File content follows below]
---