--- 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`](../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 `` entries in `webapi.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. - **`di.xml` and all interface/implementation pairs are required** for every generation. Never omit `di.xml` — without it the repository interface is unresolvable. - **`db_schema.xml` is 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.** 1. **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. 2. **Entity name** — singular, PascalCase. Infer from context (e.g. "subscriptions" → `Subscription`). Ask only if truly ambiguous. 3. **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. 4. **Operations** — which of: get, getList, save (create+update), delete, deleteById? Default to CRUD if not stated. 5. **Authentication** — admin token, customer token (`self`), anonymous, or mixed? Infer from context (see Instructions above). Ask only when genuinely ambiguous with no context clues. 6. **GraphQL** — yes/no? 7. **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 **Critical**: PHPDoc `@return` and `@param` on `Api/Data/` interface methods are parsed by Magento's SOAP/REST schema generator. Do not omit them. --- ### 2. Repository Interface — `Api/{Entity}RepositoryInterface.php` ```php 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 ``` --- ### 5. acl.xml — `etc/acl.xml` ```xml ``` --- ### 6. di.xml — `etc/di.xml` ```xml ``` --- ### 7. GraphQL Schema — `etc/schema.graphqls` ```graphql 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 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: ```bash 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] --- ```