Drupal GraphQL AI Integration Guide
Drupal GraphQL AI Integration Guide
Separation of Duties: See Separation of Duties - Drupal modules are responsible for Drupal-specific functionality. They do NOT own agent manifests, execution, or OSSA spec.
Comprehensive guide for integrating AI agents with Drupal using GraphQL 4.x architecture. This document covers schema development, agent API patterns, real-time subscriptions, and production deployment.
Overview
The Drupal GraphQL module (version 4.x) provides a powerful, developer-controlled schema builder for exposing Drupal entities and custom functionality through GraphQL APIs. This guide focuses on patterns for exposing AI agents as first-class GraphQL resources.
Key Features
- Developer-Controlled Schemas: Craft custom GraphQL schemas tailored to agent APIs
- Plugin-Based Data Producers: Extensible system for data manipulation and transformation
- GraphiQL Explorer: Built-in interactive explorer at
/graphql/explorer - Type Safety: Full support for GraphQL type system with validation
- Mutation Support: Trigger agent execution and manage agent lifecycle
- Real-Time Subscriptions: Monitor agent execution status and streaming output
- Authentication Ready: Integrate with Drupal's permission system
Architecture Overview
GraphQL Request
†
Middleware (Auth, Validation)
†
Schema Builder (Type Resolution)
†
Query Executor / Mutation Handler
†
Data Producer Plugins
†
Drupal Services / Agent Framework
†
Response Formatter
Installation & Setup
Prerequisites
- Drupal 10.2+ or 11+
- PHP 8.1 or higher
- Composer for package management
- AI Agents module for agent functionality
Installation
# Add GraphQL module via Composer composer require drupal/graphql:^4.0 # Enable the module drush en graphql # Verify installation drush gql:validate
Post-Installation Configuration
# Clear GraphQL caches drush cache:clear # Generate introspection schema drush gql:introspect > schema.graphql # Test GraphQL endpoint curl -X POST http://your-site/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { types { name } } }"}'
Schema Development
1. Creating a GraphQL Schema Module
Create a module with GraphQL schema definitions:
graphql_ai_agents/
src/
‚ Plugin/
‚ ‚ GraphQL/
‚ ‚ Schema/
‚ ‚ ‚ AgentSchema.php
‚ ‚ Type/
‚ ‚ ‚ AgentType.php
‚ ‚ ‚ AgentExecutionType.php
‚ ‚ ‚ AgentStatusType.php
‚ ‚ Mutation/
‚ ‚ ExecuteAgentMutation.php
‚ ‚ CreateAgentTaskMutation.php
‚ DataProducer/
‚ AgentLoader.php
‚ ExecutionLoader.php
‚ AgentStatusProducer.php
graphql_ai_agents.info.yml
graphql_ai_agents.module
2. Schema Plugin Definition
Define your GraphQL schema as a plugin:
<?php // src/Plugin/GraphQL/Schema/AgentSchema.php namespace Drupal\graphql_ai_agents\Plugin\GraphQL\Schema; use Drupal\graphql\Plugin\GraphQL\Schema\SchemaPluginBase; /** * The "Agent Schema" for AI agent management via GraphQL. * * @Schema( * id = "agent", * name = "Agent Schema", * description = "GraphQL schema for AI agent APIs" * ) */ class AgentSchema extends SchemaPluginBase { /** * {@inheritdoc} */ public function getQuerySchema() { return <<<GQL type Query { # Fetch a single agent by ID agent(id: ID!): Agent # List all available agents agents( first: Int after: String filter: AgentFilter ): AgentConnection! # Get agent execution history executions( agentId: ID! status: ExecutionStatus limit: Int = 10 ): [AgentExecution!]! # Check agent availability agentStatus(id: ID!): AgentStatus! } type Agent { id: ID! name: String! description: String version: String! category: String capabilities: [String!]! inputSchema: String! outputSchema: String! status: AgentStatus! createdAt: DateTime! updatedAt: DateTime! } type AgentConnection { edges: [AgentEdge!]! pageInfo: PageInfo! totalCount: Int! } type AgentEdge { node: Agent! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type AgentStatus { id: ID! available: Boolean! healthy: Boolean! lastChecked: DateTime! uptime: Float responseTime: Float } enum AgentFilter { ACTIVE INACTIVE FEATURED } GQL; } }
3. Type Definitions
Define GraphQL types for agent-related data:
<?php // src/Plugin/GraphQL/Type/AgentType.php namespace Drupal\graphql_ai_agents\Plugin\GraphQL\Type; use Drupal\graphql\Plugin\GraphQL\Type\TypePluginBase; /** * GraphQL type for Agent entities. * * @GraphQLType( * id = "agent", * name = "Agent", * description = "An AI agent" * ) */ class AgentType extends TypePluginBase {}
Define fields for the type:
<?php // src/Plugin/GraphQL/Field/AgentId.php namespace Drupal\graphql_ai_agents\Plugin\GraphQL\Field; use Drupal\graphql\Plugin\GraphQL\Field\FieldPluginBase; /** * The "id" field for Agent type. * * @GraphQLField( * id = "agent_id", * type = "ID", * parentType = "Agent", * name = "id", * description = "The agent ID" * ) */ class AgentId extends FieldPluginBase { /** * {@inheritdoc} */ public function resolveValues($value, array $args) { if ($agent = $value) { yield $agent->id(); } } }
4. Data Producer Plugins
Data producers handle complex data loading and transformation:
<?php // src/DataProducer/AgentLoader.php namespace Drupal\graphql_ai_agents\DataProducer; use Drupal\graphql\DataProducer\DataProducerPluginBase; use Drupal\ai_agents\AgentManagerInterface; /** * Data producer for loading agents. * * @DataProducer( * id = "agent_loader", * name = @Translation("Load Agent"), * description = @Translation("Loads an agent by ID"), * produces = @ContextMetadata("agent"), * consumes = { * "id" = @ContextMetadata("string") * } * ) */ class AgentLoader extends DataProducerPluginBase { /** * The agent manager. * * @var \Drupal\ai_agents\AgentManagerInterface */ protected $agentManager; /** * Constructor. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, AgentManagerInterface $agent_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->agentManager = $agent_manager; } /** * Resolve the agent by ID. */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('ai_agents.manager') ); } /** * Resolves the agent. */ public function resolve($id) { return $this->agentManager->getAgent($id); } }
Queries and Mutations
Agent Execution Mutations
Define mutations to trigger agent execution:
type Mutation { # Execute an agent synchronously executeAgent( agentId: ID! input: AgentInput! timeout: Int = 30000 ): AgentExecutionResult! # Queue an agent for asynchronous execution queueAgent( agentId: ID! input: AgentInput! priority: Int = 5 ): AgentTask! # Cancel an in-progress execution cancelExecution(executionId: ID!): Boolean! # Create a scheduled agent task scheduleAgent( agentId: ID! input: AgentInput! schedule: CronExpression! maxRuns: Int ): ScheduledTask! } input AgentInput { params: JSON! context: JSON metadata: AgentMetadata } input AgentMetadata { userId: ID source: String requestId: String timeout: Int } type AgentExecutionResult { success: Boolean! output: JSON! error: String executionTime: Float! tokens: AgentTokenUsage status: ExecutionStatus! } type AgentTokenUsage { inputTokens: Int! outputTokens: Int! totalTokens: Int! estimatedCost: Float } enum ExecutionStatus { PENDING RUNNING COMPLETED FAILED CANCELLED TIMEOUT }
Mutation Implementation
<?php // src/Plugin/GraphQL/Mutation/ExecuteAgentMutation.php namespace Drupal\graphql_ai_agents\Plugin\GraphQL\Mutation; use Drupal\graphql\Plugin\GraphQL\Mutation\MutationPluginBase; use Drupal\ai_agents\AgentManagerInterface; use Drupal\ai_agents\AgentExecutorInterface; /** * Execute an agent mutation. * * @GraphQLMutation( * id = "execute_agent", * name = "executeAgent", * type = "AgentExecutionResult", * secure = true, * description = @Translation("Execute an agent synchronously") * ) */ class ExecuteAgentMutation extends MutationPluginBase { /** * The agent manager. * * @var \Drupal\ai_agents\AgentManagerInterface */ protected $agentManager; /** * The agent executor. * * @var \Drupal\ai_agents\AgentExecutorInterface */ protected $executor; /** * Constructor. */ public function __construct( array $configuration, $plugin_id, $plugin_definition, AgentManagerInterface $agent_manager, AgentExecutorInterface $executor ) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->agentManager = $agent_manager; $this->executor = $executor; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('ai_agents.manager'), $container->get('ai_agents.executor') ); } /** * Execute the mutation. */ public function resolve() { $args = $this->getArguments(); // Load agent $agent = $this->agentManager->getAgent($args['agentId']); if (!$agent) { throw new \Exception('Agent not found'); } try { // Execute agent $execution = $this->executor->execute( $agent, $args['input']['params'], $args['input']['metadata'] ?? [] ); return [ 'success' => true, 'output' => $execution->getOutput(), 'executionTime' => $execution->getExecutionTime(), 'tokens' => $execution->getTokenUsage(), 'status' => 'COMPLETED', ]; } catch (\Exception $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'status' => 'FAILED', ]; } } }
Query Examples
# Query: Fetch agent details query GetAgent { agent(id: "claude-analyzer") { id name description capabilities status { available healthy responseTime } } } # Query: List all agents with pagination query ListAgents($first: Int!, $after: String) { agents(first: $first, after: $after) { edges { node { id name version } cursor } pageInfo { hasNextPage endCursor } totalCount } } # Mutation: Execute agent mutation ExecuteAnalyzer { executeAgent( agentId: "code-analyzer" input: { params: { code: "function hello() { console.log('hi'); }" language: "javascript" } metadata: { userId: "user-123" source: "graphql-api" } } timeout: 30000 ) { success output executionTime tokens { totalTokens estimatedCost } status } } # Mutation: Queue agent for async execution mutation QueueAgent { queueAgent( agentId: "report-generator" input: { params: { reportType: "monthly" format: "pdf" } } priority: 5 ) { id status createdAt } }
Real-Time Updates & Subscriptions
Subscription Schema
type Subscription { # Monitor agent execution progress executionProgress(executionId: ID!): ExecutionUpdate! # Stream agent output agentOutput(executionId: ID!): AgentOutput! # Monitor agent status changes agentStatusChanged(agentId: ID!): AgentStatus! # Task queue updates taskUpdated(taskId: ID!): AgentTask! } type ExecutionUpdate { executionId: ID! status: ExecutionStatus! progress: Float timestamp: DateTime! message: String } type AgentOutput { executionId: ID! type: OutputType! data: JSON! timestamp: DateTime! } enum OutputType { TEXT JSON STREAM ERROR } type AgentTask { id: ID! agentId: ID! status: ExecutionStatus! progress: Float createdAt: DateTime! startedAt: DateTime completedAt: DateTime result: JSON }
Subscription Implementation
Using WebSockets with Apollo Subscriptions:
<?php // Enable WebSocket subscriptions in graphql_ai_agents.services.yml services: graphql.subscription_manager: class: Drupal\graphql\WebSocket\SubscriptionManager arguments: - '@graphql.schema_registry' - '@event_dispatcher'
Client-Side Subscription Example
// JavaScript/TypeScript client using Apollo import { gql, useSubscription } from '@apollo/client'; const EXECUTION_SUBSCRIPTION = gql` subscription OnExecutionProgress($executionId: ID!) { executionProgress(executionId: $executionId) { executionId status progress message timestamp } } `; function ExecutionMonitor({ executionId }) { const { data, loading, error } = useSubscription( EXECUTION_SUBSCRIPTION, { variables: { executionId } } ); if (loading) return <div>Waiting for updates...</div>; if (error) return <div>Error: {error.message}</div>; const update = data?.executionProgress; return ( <div> <p>Status: {update?.status}</p> <progress value={update?.progress} max="100" /> <p>{update?.message}</p> </div> ); }
Authentication & Authorization
GraphQL Authentication Middleware
<?php // src/GraphQL/AuthenticationMiddleware.php namespace Drupal\graphql_ai_agents\GraphQL; use Drupal\Core\Authentication\AuthenticationProviderInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\HttpFoundation\Request; /** * GraphQL authentication middleware. */ class AuthenticationMiddleware implements AuthenticationProviderInterface { /** * {@inheritdoc} */ public function applies(Request $request) { return $request->getPathInfo() === '/graphql'; } /** * {@inheritdoc} */ public function authenticate(Request $request) { // Extract token from Authorization header $token = $this->extractToken($request); if (!$token) { return NULL; } // Validate token return $this->validateToken($token); } /** * Extract bearer token from request. */ private function extractToken(Request $request) { $header = $request->headers->get('Authorization', ''); if (preg_match('/^Bearer\s+(.+)$/', $header, $matches)) { return $matches[1]; } return NULL; } }
Permission Checking in Mutations
<?php // In mutation resolver public function resolve() { // Check user has permission if (!$this->currentUser->hasPermission('execute ai agents')) { throw new \Exception('You do not have permission to execute agents'); } // Check agent-specific permissions $agent = $this->agentManager->getAgent($args['agentId']); if (!$agent->access('execute', $this->currentUser)) { throw new \Exception('Access denied for this agent'); } // Proceed with execution... }
Error Handling
GraphQL Error Responses
{ "data": { "executeAgent": null }, "errors": [ { "message": "Agent not found", "extensions": { "code": "AGENT_NOT_FOUND", "agentId": "unknown-agent" } } ] }
Error Handler Plugin
<?php // src/Plugin/GraphQL/ErrorHandler.php namespace Drupal\graphql_ai_agents\Plugin\GraphQL; use GraphQL\Error\Error; /** * Custom error handler for GraphQL. */ class ErrorHandler { /** * Format GraphQL errors. */ public static function handle(Error $error) { $previous = $error->getPrevious(); if ($previous instanceof AgentNotFoundException) { return [ 'message' => 'Agent not found', 'extensions' => [ 'code' => 'AGENT_NOT_FOUND', 'agentId' => $previous->getAgentId(), ], ]; } if ($previous instanceof ExecutionTimeoutException) { return [ 'message' => 'Execution timeout', 'extensions' => [ 'code' => 'EXECUTION_TIMEOUT', 'timeout' => $previous->getTimeout(), ], ]; } // Generic error return [ 'message' => $error->getMessage(), 'extensions' => ['code' => 'INTERNAL_ERROR'], ]; } }
Code Examples
Complete Agent Execution Flow
<?php // src/Service/AgentGraphQLExecutor.php namespace Drupal\graphql_ai_agents\Service; use Drupal\ai_agents\AgentManagerInterface; use Drupal\ai_agents\AgentExecutorInterface; use Drupal\Core\Entity\EntityStorageInterface; /** * Service for executing agents via GraphQL. */ class AgentGraphQLExecutor { /** * Constructor. */ public function __construct( private AgentManagerInterface $agentManager, private AgentExecutorInterface $executor, private EntityStorageInterface $executionStorage ) {} /** * Execute an agent synchronously. */ public function executeSync($agentId, array $input, array $metadata = []) { // Load agent $agent = $this->agentManager->getAgent($agentId); if (!$agent) { throw new \Exception("Agent '{$agentId}' not found"); } // Validate input against schema $this->validateInput($agent, $input); // Execute $start_time = microtime(true); try { $result = $this->executor->execute($agent, $input, $metadata); $execution_time = microtime(true) - $start_time; // Store execution record $execution = $this->executionStorage->create([ 'agent_id' => $agentId, 'status' => 'completed', 'input' => $input, 'output' => $result->getOutput(), 'execution_time' => $execution_time, 'tokens' => $result->getTokenUsage(), ]); $execution->save(); return [ 'success' => true, 'output' => $result->getOutput(), 'executionTime' => $execution_time, 'tokens' => $result->getTokenUsage(), 'status' => 'COMPLETED', ]; } catch (\Exception $e) { // Store error $execution = $this->executionStorage->create([ 'agent_id' => $agentId, 'status' => 'failed', 'input' => $input, 'error' => $e->getMessage(), ]); $execution->save(); return [ 'success' => false, 'error' => $e->getMessage(), 'status' => 'FAILED', ]; } } /** * Queue an agent for async execution. */ public function queueAsync($agentId, array $input, $priority = 5) { $task = $this->executionStorage->create([ 'agent_id' => $agentId, 'status' => 'pending', 'input' => $input, 'priority' => $priority, ]); $task->save(); return $task; } /** * Validate input against agent schema. */ private function validateInput($agent, array $input) { $schema = $agent->getInputSchema(); // Validate using JSON Schema or Zod if (!$this->validator->validate($input, $schema)) { throw new \Exception('Invalid input: ' . $this->validator->getError()); } } }
GraphQL Testing
<?php // tests/src/Functional/GraphQLAgentTest.php namespace Drupal\Tests\graphql_ai_agents\Functional; use Drupal\Tests\graphql\Functional\GraphQLTestBase; /** * Test GraphQL agent execution. * * @group graphql_ai_agents */ class GraphQLAgentTest extends GraphQLTestBase { /** * Test executing an agent via GraphQL. */ public function testExecuteAgent() { $query = <<<GQL mutation { executeAgent( agentId: "test-agent" input: { params: { text: "Hello, world!" } } ) { success output status } } GQL; $result = $this->query($query); $this->assertTrue($result['data']['executeAgent']['success']); $this->assertEqual('COMPLETED', $result['data']['executeAgent']['status']); } /** * Test querying agent status. */ public function testQueryAgentStatus() { $query = <<<GQL query { agent(id: "test-agent") { id name status { available healthy } } } GQL; $result = $this->query($query); $this->assertNotEmpty($result['data']['agent']); $this->assertEqual('test-agent', $result['data']['agent']['id']); } }
Best Practices
1. Schema Design
- Keep schemas focused and minimal
- Use clear, descriptive field names
- Document all types and fields
- Version your schema for backward compatibility
2. Performance
- Implement field-level caching
- Use batch loading for N+1 query prevention
- Set reasonable query depth limits
- Monitor query complexity
3. Security
- Validate all inputs
- Implement rate limiting per agent
- Log all executions
- Use authentication tokens with expiration
- Implement CORS properly
4. Error Handling
- Provide meaningful error messages
- Include error codes for client handling
- Log errors for debugging
- Don't expose internal implementation details
5. Documentation
- Use GraphQL descriptions
- Provide example queries
- Document authentication flow
- Maintain changelog for schema updates
Deployment
Production Checklist
- All queries tested with real data
- Mutations include proper validation
- Authentication configured and tested
- Rate limiting enabled
- Error handling covers all cases
- Performance tested under load
- Documentation updated
- Security audit completed
Performance Tuning
# graphql_ai_agents.settings.yml graphql: # Cache field results field_cache: enabled: true ttl: 3600 # Query complexity limits query_limit: max_depth: 10 max_complexity: 1000 # Batch loading batch_load: enabled: true batch_size: 20 # Rate limiting rate_limit: enabled: true requests_per_minute: 100 requests_per_hour: 1000
Resources
- Drupal GraphQL Module: https://www.drupal.org/project/graphql
- GraphQL.org: https://graphql.org
- Drupal GraphQL Book: https://drupal-graphql.gitbook.io/graphql/
- GraphQL Best Practices: https://graphql.org/learn/best-practices/
- AI Agents Module: See AI Agents Orchestra