Skip to content

Custom Scopes

You can extend the scope system by creating custom scope plugins in your own modules.

Creating a custom scope plugin

  1. Create a class that extends AiContextScopeBase
  2. Place it in src/Plugin/AiContextScope/ within your module
  3. Add the #[AiContextScope] attribute with plugin metadata
  4. Implement getValues() to return available scope values
  5. Implement doGetCurrentValue() (return NULL when the scope has no contextual auto-detection)

Example

namespace Drupal\my_module\Plugin\AiContextScope;

use Drupal\ai_context\Attribute\AiContextScope;
use Drupal\ai_context\Plugin\AiContextScope\AiContextScopeBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;

#[AiContextScope(
  id: 'department',
  label: new TranslatableMarkup('Department'),
  description: new TranslatableMarkup('Department this context applies to.'),
  weight: 25,
)]
class AiContextScopeDepartment extends AiContextScopeBase {

  public function getValues(): array {
    return [
      'marketing' => $this->t('Marketing'),
      'engineering' => $this->t('Engineering'),
      'sales' => $this->t('Sales'),
    ];
  }

  protected function doGetCurrentValue(): ?string {
    return NULL;
  }

}

The plugin is automatically discovered by the AiContextScopeManager. Clear caches after adding a new plugin.

Storage model for custom scopes (1.0)

Custom scope plugins in 1.0 should store values in the context item scope map field. Extend AiContextScopeBase and use the default getValuesFromEntity() implementation (which resolves parent inheritance via getScopeItem()).

Custom entity-field storage, delete cleanup, and form lifecycle hooks are not part of the public extension contract in 1.0. Map-backed custom scopes are indexed automatically; do not hook the index pipeline directly. The built-in entity item scope uses a separate DER field as an internal implementation detail.

Automatic routes and settings tabs

When a scope plugin is discovered, the module automatically generates:

  • A settings route named ai_context.settings.scope.{plugin_id} at the URL path /admin/config/ai/context/settings/scope/{plugin-id}.
  • A local task tab under the Scope settings page, using the plugin's label as the tab title.

Custom scopes are interleaved with built-in scopes according to their declared weight in context item forms, the Details panel, and scope settings tabs. Higher weights appear first, with plugin ID used to break ties. You do not need to define routes or local tasks manually.

Plugin ID vs URL path

The plugin ID and the URL path use slightly different conventions:

  • Route name keeps the plugin ID verbatim, including underscores. For example, the entity_item scope produces the route name ai_context.settings.scope.entity_item.
  • URL path converts underscores to dashes for readability, so the same scope is served at /admin/config/ai/context/settings/scope/entity-item.

When linking to a scope settings page from your own code (for example, from getManageRoute()), always use the route name with underscores -- never the URL path:

public function getManageRoute(): ?array {
  return ['route_name' => 'ai_context.settings.scope.entity_item'];
}

When the route has placeholders, also include route_parameters (see the getManageRoute() examples below).

If a scope plugin is removed (e.g., by uninstalling a module or via hook_ai_context_scope_info_alter()), its route and tab are removed automatically. The scope plugin manager invalidates the local_task cache tag whenever its definitions are cleared, so derived tabs stay in sync without a full cache rebuild.

Attribute parameters

Parameter Type Description
id string Unique plugin ID
label TranslatableMarkup Human-readable label
description TranslatableMarkup\|null General description, shown on the scope overview and settings pages. See Choosing a description method to give context item forms different text.
weight int Display and subscription scoring weight. Higher values appear earlier in scope forms and settings and have greater scoring influence. Custom scopes can use intermediate values (for example, 65 between Use Case and Entity Types). Weights below 1 are treated as 1 during subscription scoring only; display order still uses the declared weight. Default: 1. See Scope API for the built-in weight list.
deriver string\|null Optional deriver class
icon_class string\|null Optional CSS class for UI icons (e.g. ai-icon--scope-use-case)

Scope icons

Scope plugins can declare an optional icon_class on the #[AiContextScope] attribute. That is the supported way to associate an icon with your scope.

For your custom scope, declare icon_class on the plugin and use that CSS class in your render array or form element. Core scope icon resolution is module-internal; contrib UI should use scope labels or your own icons rather than calling getScopeIconClass() or injecting plugin.manager.ai_context_scope (see API stability).

Core scopes ship Phosphor SVG icons under icons/scope/ and CSS classes in css/ai_context_item.css. Custom scopes must ship their own CSS (and SVG assets if needed) for any icon_class they declare or set via the alter hook. The attribute value is the CSS class name; your render array, form, or theme hook must attach the library that defines those styles (for example #attached['library'][] = 'my_module/scope_icons').

#[AiContextScope(
  id: 'department',
  label: new TranslatableMarkup('Department'),
  icon_class: 'ai-icon--scope-department',
)]
class AiContextScopeDepartment extends AiContextScopeBase {
  // ...
}

Built-in scope plugins define a PLUGIN_ID constant equal to their plugin ID. Reference those constants when interacting with core scopes from module code.

Optional overrides

The base class provides sensible defaults. Override these methods to customize behavior:

supportsSubscriptions()

Return FALSE to hide this scope from agent configuration forms. The same flag also affects internal context auto-inclusion: when FALSE and matchesCurrentContext() returns TRUE, the item may be included without an agent subscription (see Agent subscriptions in the Scope API). Useful for scopes that apply automatically (like Global or Entity Item).

public function supportsSubscriptions(): bool {
  return FALSE;
}

allowsMultiple()

Return FALSE to render radio buttons instead of checkboxes (single selection).

isDynamic()

Return TRUE if values come from an external source (database, config, API) rather than being hardcoded. Dynamic scopes may change at runtime.

getCurrentValue()

Return the currently active value for contextual detection. For example, the Language scope returns the current language, and the Site Section scope returns the matching section for the current URL.

getValuesFromEntity()

Return the scope values configured on a context item. The base implementation resolves effective scope through AiContextItem::getScopeItem(), so a subcontext item that inherits its parent's scope returns the parent's values:

public function getValuesFromEntity(AiContextItem $item): array {
  return $item->getScopeItem()->getScopeValues($this->getId());
}

If you override this method, keep resolving through getScopeItem(). Reading $item->getScopeValues() (or $item->getScope()) directly bypasses parent inheritance: for an inheriting child the item's own scope map is empty, so your scope would contribute nothing while the other scopes still reflect the parent — an inconsistent effective scope, and the selector and scope index would disagree. See Effective scope and parent inheritance.

matchesCurrentContext()

Implement custom matching logic for how this scope determines whether a context item matches the current request context.

getManageRoute() and getManageLabel()

Provide a link to an admin page where this scope's values can be managed. Return a route info array from getManageRoute(), and the link text from getManageLabel().

Most scope settings routes need only route_name:

public function getManageRoute(): ?array {
  return ['route_name' => 'ai_context.settings.scope.entity_item'];
}

When the route has placeholders, include route_parameters as well (as the Tag scope does for its vocabulary overview page):

public function getManageRoute(): ?array {
  return [
    'route_name' => 'entity.taxonomy_vocabulary.overview_form',
    'route_parameters' => ['taxonomy_vocabulary' => 'ai_context_tags'],
  ];
}

buildSettingsForm(), validateSettingsForm(), submitSettingsForm()

Add custom settings beyond the default enabled checkbox.

For full API details, see the Scope API reference.

Altering scope values

Use hook_ai_context_scope_values_alter() to add, modify, or remove values from any scope plugin without writing a full plugin. See Hooks.

function my_module_ai_context_scope_values_alter(
  array &$values,
  string $scope_id,
): void {
  if ($scope_id === 'use_case') {
    $values['custom_workflow'] = t('Custom Workflow');
  }
}

Parameters:

  • $values -- associative array of value_id => label pairs (passed by reference)
  • $scope_id -- the plugin ID of the scope being altered (e.g., use_case, language, site_section)