Install
$ agentstack add skill-trebormc-drupal-ai-agents-drupal-code-patterns ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
About the "Reference:" paths below
Some patterns point to the contrib Examples module for extended reference. These templates are SELF-SUFFICIENT — the references are optional extra reading. Before following one, check it exists:
ssh web test -d $DDEV_DOCROOT/modules/contrib/examples && echo "examples available" || echo "examples not installed — skip the references"
Service Class
logger->info('Processing: @input', ['@input' => $input]);
return ['result' => $input];
}
}
Form (FormBase)
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/form_api_example
get('mymodule.my_service'));
}
public function getFormId(): string {
return 'mymodule_my_form';
}
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#required' => TRUE,
'#maxlength' => 255,
];
$form['actions'] = [
'#type' => 'actions',
'submit' => ['#type' => 'submit', '#value' => $this->t('Submit')],
];
return $form;
}
public function validateForm(array &$form, FormStateInterface $form_state): void {
if (strlen($form_state->getValue('name')) setErrorByName('name', $this->t('Name must be at least 3 characters.'));
}
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->myService->process($form_state->getValue('name'));
$this->messenger()->addStatus($this->t('Form submitted successfully.'));
}
}
Block Plugin
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/block_example
get('mymodule.my_service'));
}
public function build(): array {
return [
'#theme' => 'mymodule_block',
'#data' => $this->myService->getData(),
'#cache' => [
'contexts' => ['user'],
'tags' => ['mymodule:data'],
'max-age' => 3600,
],
];
}
}
Routing & Controllers
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/page_example
mymodule.routing.yml
mymodule.example_page:
path: '/mymodule/example/{parameter}'
defaults:
_controller: '\Drupal\mymodule\Controller\ExampleController::content'
_title: 'Example Page'
parameter: 'default_value'
requirements:
_permission: 'access content'
parameter: '\d+'
mymodule.form_page:
path: '/mymodule/form'
defaults:
_form: '\Drupal\mymodule\Form\MyForm'
_title: 'My Form'
requirements:
_permission: 'access mymodule'
Controller
get('mymodule.my_service'));
}
public function content(string $parameter): array {
return [
'#theme' => 'mymodule_example',
'#data' => $this->myService->getData($parameter),
'#cache' => [
'contexts' => ['url.path'],
'tags' => ['mymodule:data'],
'max-age' => 3600,
],
];
}
}
Hooks
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/hooks_example
[
'variables' => ['title' => '', 'items' => []],
'template' => 'mymodule-custom',
],
];
}
Caching Best Practices
Every render array MUST have cache metadata:
public function build(): array {
return [
'#markup' => $this->getData(),
'#cache' => [
'contexts' => ['user', 'url.path', 'url.query_args'],
'tags' => ['node:1', 'node_list', 'mymodule:data'],
'max-age' => 3600,
],
];
}
For dynamic user-specific content, use lazy builders:
$build['dynamic_part'] = [
'#lazy_builder' => ['mymodule.lazy_builder:build', [$entity_id]],
'#create_placeholder' => TRUE,
];
For comprehensive caching strategies, use the performance-audit skill.
Batch API
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/batch_example
function mymodule_batch_process(array $items, array &$context): void {
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($items);
}
$batch_size = 10;
$slice = array_slice($items, $context['sandbox']['progress'], $batch_size);
foreach ($slice as $item) {
// Process item...
$context['sandbox']['progress']++;
}
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
function mymodule_execute_batch(): void {
$batch = [
'title' => t('Processing items...'),
'operations' => [['mymodule_batch_process', [range(1, 1000)]]],
'finished' => 'mymodule_batch_finished',
'progress_message' => t('Processed @current of @total.'),
];
batch_set($batch);
}
Queue API
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/queue_example
mail(
'mymodule', 'notification', $data['to'], $data['langcode'], $data['params']
);
}
}
Adding items: \Drupal::queue('mymodule.email_sender')->createItem($data);
AJAX Forms
Reference: $DDEV_DOCROOT/modules/contrib/examples/modules/ajax_example
'select',
'#title' => $this->t('Category'),
'#options' => ['fruits' => $this->t('Fruits'), 'vegetables' => $this->t('Vegetables')],
'#ajax' => [
'callback' => '::updateItemsCallback',
'wrapper' => 'items-wrapper',
'event' => 'change',
],
];
$form['items_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'items-wrapper'],
];
$category = $form_state->getValue('category', 'fruits');
$form['items_wrapper']['item'] = [
'#type' => 'select',
'#title' => $this->t('Item'),
'#options' => $this->getItemsByCategory($category),
];
$form['actions'] = [
'#type' => 'actions',
'submit' => ['#type' => 'submit', '#value' => $this->t('Submit')],
];
return $form;
}
public function updateItemsCallback(array &$form, FormStateInterface $form_state): array {
return $form['items_wrapper'];
}
private function getItemsByCategory(string $category): array {
$items = [
'fruits' => ['apple' => 'Apple', 'banana' => 'Banana'],
'vegetables' => ['carrot' => 'Carrot', 'broccoli' => 'Broccoli'],
];
return $items[$category] ?? [];
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->messenger()->addStatus($this->t('Form submitted.'));
}
}
After Applying Any Pattern
# 1. Register new classes/files and rebuild caches:
ssh web drush cr
# 2. If you added/changed a route, verify it exists:
ssh web drush route | grep mymodule
# 3. Run quality checks (see the quality-checks skill) before presenting the code.
Where to see the result: forms/controllers at their route path, blocks via Block Layout (/admin/structure/block) after placing them.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: trebormc
- Source: trebormc/drupal-ai-agents
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.