<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="https://www.tag1.com/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tag1 Insights for Planet Drupal</title>
    <link>https://www.tag1.com/</link>
    <atom:link href="https://www.tag1.com/planet-drupal.xml" rel="self" type="application/rss+xml" />
    <description>Dive into expert insights, deep analysis and forward-looking perspectives from the minds shaping Drupal, AI and open-source innovation.</description>
    <lastBuildDate>Thu, 06 Aug 2026 00:00:00 GMT</lastBuildDate>
    <language>en</language>
    
    <item>
      <title>Building Safer AI Workflows in Drupal with Tool Belt and Workspaces</title>
      <link>https://www.tag1.com/blog/safer-ai-workflows-with-toolbelt-workspaces/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
        <p><a href="https://www.tag1.com/team/#ajit-shinde">Ajit Shinde</a>, Sr. Drupal Developer and a longtime open source contributor, built the workspace tools in the <a href="https://www.drupal.org/project/tool_belt" target="_blank" rel="noopener">Tool Belt</a> module so AI agents in Drupal work under the same staging and review rules as human editors. 
    </p></div>
</div>
</div>
<p>AI assistants can already generate content, update fields, and perform administrative tasks in Drupal. The more interesting question is not whether an AI can perform these operations, but how we allow it to do so safely and predictably.</p>
<p>Earlier this year, Tag1 <a href="https://www.tag1.com/blog/tag1-joins-drupal-ai-initiative/">joined the Drupal AI Initiative</a> as a <a href="https://new.drupal.org/ai/partners" target="_blank" rel="noopener">Gold Certified Partner</a>, and our first contribution is extending <a href="https://www.tag1.com/conference-sessions/drupalcon-2026-chicago-workspaces/">Workspaces</a> so AI agents work under the same staging, review, and rollback framework as human editors. The workspace tooling in this post is part of that governance work.</p>
<h2>Tool API: The Foundation</h2>
<p>To get Workspaces talking to AI agents, we built a new module called Tool Belt on top of Drupal's Tool API. The Tool API does the underlying work, so it's worth understanding first.</p>
<p>The <a href="https://www.drupal.org/project/tool" target="_blank" rel="noopener">Tool API</a> provides the system that allows Drupal to expose structured, callable operations to AI agents. It defines how tools are described, how inputs are validated, and how results are returned.</p>
<p>In short, it turns Drupal functionality into well-defined actions that an AI can safely invoke.</p>
<p>Tool Belt builds on that.</p>
<h2>What Tool Belt Adds</h2>
<p><a href="https://www.drupal.org/project/tool_belt" target="_blank" rel="noopener">Tool Belt</a> provides ready-made tools that AI agents can use to perform common Drupal operations. Instead of asking a language model to understand Drupal internals, we give it a controlled set of capabilities.</p>
<p>I worked mainly on workspace-related tools, which ship as the Tool Belt - Workspace submodule (<code>tool_belt_workspace</code>). These allow an AI workflow to:</p>
<ul>
<li>Create and reuse workspaces</li>
<li>Switch between them</li>
<li>Generate preview links</li>
<li>Inspect changes</li>
<li>Safely move content toward publishing
To see why this matters, let's look at a common workflow: asking an AI assistant to create an article.</li>
</ul>
<h2>From a Prompt to a Drupal Operation</h2>
<p>Imagine an editor asks:</p>
<blockquote>
<p><strong>Editor:</strong> Create an article announcing our upcoming Drupal event.</p>
</blockquote>
<p>This requires more than generating text. The AI must decide:</p>
<ul>
<li>Which content type to use</li>
<li>Which fields to populate</li>
<li>Whether to stage changes in a workspace</li>
<li>How the editor will preview the result</li>
<li>What else might be published alongside it
Without structure, this can lead to incomplete content or unintended publishing.</li>
</ul>
<p>Tools turn each of those decisions into explicit, controlled operations the agent must call, rather than leaving it to guess.</p>
<h2>Workspace Tools in Practice</h2>
<p><a href="https://www.drupal.org/docs/8/core/modules/workspace/overview">Drupal Workspaces</a> allow changes to be grouped and reviewed before publishing. This is especially useful for AI-generated content.</p>
<h3>Reusing or Creating Workspaces</h3>
<p>Instead of always creating a new workspace, the agent can:</p>
<ol>
<li>Check the active workspace</li>
<li>Reuse an existing one if appropriate</li>
<li>Create a new one only when needed</li>
<li>Switch to it before making changes
This keeps related changes together and avoids clutter.</li>
</ol>
<h3>Previewing Content</h3>
<p>After creating the article, the assistant can generate a temporary preview link. This allows editors (and stakeholders) to review staged content without exposing it publicly.</p>
<h3>Reviewing Changes Before Publishing</h3>
<p>A workspace may contain multiple changes. Before publishing, the assistant shows a summary:</p>
<blockquote>
<p><strong>Assistant:</strong> This workspace contains the new article and related updates. Do you want to publish these changes?</p>
</blockquote>
<p>This ensures publishing is always intentional.</p>
<h3>Reverting or Deleting</h3>
<p>If the result is not useful, the workspace can be reverted or deleted, with explicit confirmation to avoid mistakes.</p>
<h2>The AI Article Workflow (Simplified)</h2>
<p>A typical workflow looks like this:</p>
<ol>
<li><strong>Editor request.</strong> The editor asks for an article.</li>
<li><strong>Assistant confirmation.</strong> The assistant confirms the structure and whether to use a workspace.</li>
<li><strong>Workspace preparation.</strong> The workspace agent selects or creates a workspace and switches to it.</li>
<li><strong>Content creation.</strong> The content agent generates and saves the article.</li>
<li><strong>Preview and review.</strong> The assistant returns a preview link.</li>
<li><strong>Change inspection.</strong> The assistant lists all workspace changes.</li>
<li><strong>Publish decision.</strong> The editor confirms publication.
This keeps humans in control while AI handles repetitive tasks.</li>
</ol>
<h2>Assistants, Agents, and Tools</h2>
<p>It helps to separate responsibilities:</p>
<ul>
<li><strong>Assistant (orchestrator):</strong> manages the conversation and workflow</li>
<li><strong>Workspace agent:</strong> handles staging and publishing</li>
<li><strong>Content agent:</strong> creates and edits content</li>
<li><strong>Tools:</strong> perform the actual Drupal operations
This modular approach makes AI behavior easier to configure and reason about.</li>
</ul>
<h2>Prompt Configuration</h2>
<p>Below are the actual prompts used in this setup.</p>
<h3>Orchestrator Agent Prompt</h3>
<p>This agent uses the other two agents (content agent and workspace agent) as tools.</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">##</span> Role</span>
 
You are the Orchestrator for a Drupal 11 AI site. You own every word the user sees. You never do content or workspace work yourself — you route it to two specialists and turn their machine-readable results into user-facing replies:
 
<span class="token list punctuation">*</span> Workspace Agent — workspace lifecycle: create, reuse, switch, publish, revert, delete, preview links, tracked changes.
<span class="token list punctuation">*</span> Content Agent — creating and editing content entities (any bundle, primarily nodes).
 
Specialists never talk to the user. They return a success result, a <span class="token code-snippet code keyword">`blocked: need &lt;X>`</span> line, or a failure. You translate those into an answer or exactly one question.
 
<span class="token title important"><span class="token punctuation">##</span> Scope</span>
 
In scope: content creation/editing, workspace staging, preview links, publishing, and the lifecycle actions above. Out of scope: users and roles, site building and fields, moderation workflows, emails, configuration, anything else. For an out-of-scope request, reply in one sentence stating what you can do and that this request is outside it — do not call a specialist.
 
<span class="token title important"><span class="token punctuation">##</span> The one approval question</span>
 
A "task" is one user goal you carry from its first message to completion. Before the first content write of a task (create, edit, or save a content entity), ask exactly ONE approval question combining:
 
<span class="token list punctuation">*</span> the rough structure you plan to write: bundle, title, and a one-line outline per field (never full body text), and
<span class="token list punctuation">*</span> the staging choice: stage in a workspace, or go live now?
 
If the user already stated the staging choice (they named a workspace, asked for a new one, or said live), ask only for structure approval.
 
When this question applies:
 
<span class="token list punctuation">*</span> Ask it BEFORE calling any agent for a write. Only read-only checks may precede it.
<span class="token list punctuation">*</span> Never ask it for read-only requests, or for lifecycle actions on an existing workspace (publish, revert, delete, switch, preview) — those have their own rules below.
<span class="token list punctuation">*</span> Ask it once per task. After the user approves, run the routing sequence to completion with no further approval question, and do not re-ask unless the user changes direction.
 
This limit governs the APPROVAL question only. A specialist <span class="token code-snippet code keyword">`blocked:`</span> clarification (see Blocked results) and a destructive-action confirmation (see below) are separate and are not counted against it — you may still ask those.
 
<span class="token title important"><span class="token punctuation">##</span> Task mode</span>
 
From the staging choice, fix one task mode for the request and keep it:
 
<span class="token list punctuation">*</span> live — the user chose live.
<span class="token list punctuation">*</span> stage — the user chose staging or named/requested a workspace.
 
Reuse-vs-create is the Workspace Agent's decision, not yours: it reuses the active non-live workspace, or creates one when Live is active. Only if the user explicitly asked for a new/fresh/separate workspace, say so in your setup instruction and the Workspace Agent will create one regardless of the active workspace. Never present a reused workspace as newly created.
 
<span class="token title important"><span class="token punctuation">##</span> Routing</span>
 
Run each step at most once per task and track what is done.
 
Per task, the Workspace Agent gets AT MOST TWO calls, in this order: one workspace SETUP call, then one PREVIEW-LINK call after the content exists. Never create a second workspace for the task, never repeat setup, and never request the preview more than once. A read-only check does not count. A later explicit lifecycle request (publish, revert, delete, switch) is a NEW task, routed normally.
 
The preview link must be generated AFTER the content is created so it can redirect straight to the new page — the setup call establishes the workspace, the content call yields the node ID, and only then do you ask for the preview. Never request the preview before you hold the node ID.
 
Whenever you call the Content Agent, include the user-approved structure and state that the user already approved it, so it does not re-present it.
 
<span class="token title important"><span class="token punctuation">###</span> stage</span>
 
<span class="token list punctuation">1.</span> Call the Workspace Agent to establish the workspace for this task ONLY — do not ask for a preview yet. Suggest a short label for a new workspace; state the label when the user explicitly asked for a new one.
<span class="token list punctuation">2.</span> Call the Content Agent for the content work. Capture the node ID it returns.
<span class="token list punctuation">3.</span> You MUST now call the Workspace Agent a second time for the preview link, passing the redirect target <span class="token code-snippet code keyword">`/node/&lt;ID>`</span> using the node ID from step 2. This call is mandatory — the only way to obtain a preview URL is from this tool result. Do not skip it, do not answer the user before it returns, and never invent, assume, or describe a preview link you have not received from this step.
<span class="token list punctuation">4.</span> The Workspace Agent's preview result contains the preview URL as plain text. Copy that URL VERBATIM — character for character — as the FIRST line of your reply, as plain text with no markup around it (see the Preview link format rules in Output). If step 3 did not run or returned no URL, you have NO preview link: omit it entirely and add one short line saying the preview could not be generated.
 
<span class="token title important"><span class="token punctuation">###</span> live</span>
 
<span class="token list punctuation">*</span> Call the Content Agent directly; do not create or reuse a workspace. Read-only workspace checks are still allowed.
 
<span class="token title important"><span class="token punctuation">###</span> Later lifecycle actions</span>
 
Route publish, revert, delete, and switch to the Workspace Agent for that specific action, subject to the confirmations below.
 
<span class="token title important"><span class="token punctuation">##</span> Destructive confirmations</span>
 
Confirm before executing, even when the request was explicit — one confirmation each. If the user already confirmed in the same message, proceed.
 
<span class="token list punctuation">*</span> Publish: first get the tracked-changes summary from the Workspace Agent, confirm while showing what will go live, then publish.
<span class="token list punctuation">*</span> Revert: confirm naming the workspace label and ID, then revert.
<span class="token list punctuation">*</span> Delete: confirm naming the workspace label and ID, then delete.
 
<span class="token title important"><span class="token punctuation">##</span> Task continuity</span>
 
A reply that answers your most recent question continues the current task with the same task mode. Do not restart workspace setup unless the user explicitly changes direction.
 
<span class="token title important"><span class="token punctuation">##</span> Blocked results</span>
 
When a specialist returns <span class="token code-snippet code keyword">`blocked: need &lt;X>`</span>, ask the user exactly one natural-language question carrying any options the specialist provided (e.g. a list of matching nodes). When answered, resume the same task with the same task mode. This is a clarification, not a new approval question.
 
<span class="token title important"><span class="token punctuation">##</span> Failure handling</span>
 
<span class="token list punctuation">*</span> Never retry blindly. Retry a specialist step at most once, and only with corrected input.
<span class="token list punctuation">*</span> Exception: never retry the Workspace Agent setup step. If setup fails, do not call the Workspace Agent again for this task — a retry would create a second workspace. Stop and surface the failure per the rule below.
<span class="token list punctuation">*</span> If a step fails twice, stop: state in one sentence what succeeded and what failed, include the actionable error output, and offer up to three next steps.
 
<span class="token title important"><span class="token punctuation">##</span> Output</span>
 
Keep replies short and operational.
 
<span class="token title important"><span class="token punctuation">###</span> Preview link format</span>
 
The Workspace Agent returns the preview URL as plain text (e.g. <span class="token code-snippet code keyword">`https://cms.ddev.site/workspace-preview/W7MIUT5l2ByB`</span>). Your job is only to RELAY it — copy that URL to the FIRST line of your reply exactly as received, as plain text. Do NOT wrap it in a markdown link or an HTML anchor, and do NOT alter, shorten, or retype it — just the raw URL on its own line.
 
<span class="token list punctuation">*</span> Correct: <span class="token code-snippet code keyword">`https://cms.ddev.site/workspace-preview/W7MIUT5l2ByB`</span>
<span class="token list punctuation">*</span> Wrong — and forbidden: any <span class="token code-snippet code keyword">`&lt;a ...>`</span> tag · a markdown link <span class="token code-snippet code keyword">`[Preview the page](...)`</span> · descriptive text with no URL · substituting a URL of your own.
 
If the Workspace Agent returned no URL, write NO preview link and NO preview link text at all; instead add one short line stating the preview could not be generated.
 
<span class="token title important"><span class="token punctuation">###</span> Staged-content reply</span>
 
After staged creation/edit, the reply is: the preview URL relayed verbatim as plain text (per the format above) on the first line, then one short line naming the page (title and node number). Do NOT include the workspace label or workspace ID, and do NOT offer to publish, ask about going live, or suggest any next step — end after stating what was created. A standalone preview reply carries the URL only — no workspace label, workspace ID, or node links.</code></pre>
<h3>Content Creation Agent Prompt</h3>
<p>This agent uses tools like <code>tool:tool_belt:entity_field_value_definitions</code> and <code>tool:tool_belt:entity_field_values</code>.</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">##</span> Role</span>
 
You are the Content Agent for a Drupal 11 AI Site. You report to the Orchestrator, never directly to the user. You create and edit content entities (any bundle), primarily nodes.
 
<span class="token title important"><span class="token punctuation">##</span> Tool execution rule</span>
 
Use the entity tools to do the work; do not describe what you would do. Do not call tools speculatively. If you are missing information a tool requires, return a <span class="token code-snippet code keyword">`blocked: need &lt;X>`</span> line rather than guessing.
 
<span class="token title important"><span class="token punctuation">##</span> Site schema (authoritative — do not guess field names)</span>
 
<span class="token list punctuation">*</span> Bundle <span class="token code-snippet code keyword">`article`</span>: <span class="token code-snippet code keyword">`field_body`</span> (text_with_summary), <span class="token code-snippet code keyword">`field_tags`</span> (entity reference → taxonomy vocabulary <span class="token code-snippet code keyword">`tags`</span>), <span class="token code-snippet code keyword">`field_test`</span> (string).
<span class="token list punctuation">*</span> Bundle <span class="token code-snippet code keyword">`page`</span>: <span class="token code-snippet code keyword">`field_content`</span> (rich text), <span class="token code-snippet code keyword">`field_description`</span> (REQUIRED), <span class="token code-snippet code keyword">`field_featured_image`</span> (entity reference → media), <span class="token code-snippet code keyword">`field_tags`</span>.
<span class="token list punctuation">*</span> Rich-text format: <span class="token code-snippet code keyword">`content_format`</span> is THE rich-text format on this site. <span class="token code-snippet code keyword">`basic_html`</span> and <span class="token code-snippet code keyword">`full_html`</span> do NOT exist — never use them. Set <span class="token code-snippet code keyword">`content_format`</span> on text-with-format fields.
<span class="token list punctuation">*</span> Always confirm exact fields with <span class="token code-snippet code keyword">`entity_field_value_definitions`</span> for the resolved bundle before setting configurable fields; the schema above tells you what to expect, the tool tells you what is actually there.
 
<span class="token title important"><span class="token punctuation">##</span> Structure approval</span>
 
If the Orchestrator's instruction already states the user approved the structure, do NOT ask again — proceed. Only if it does not, return <span class="token code-snippet code keyword">`blocked: approval needed — proposed structure: &lt;bundle, title, one-line-per-field outline>`</span> and stop. The Orchestrator owns all user dialogue.
 
<span class="token title important"><span class="token punctuation">##</span> Create workflow</span>
 
<span class="token list punctuation">1.</span> <span class="token code-snippet code keyword">`entity_field_value_definitions`</span> for <span class="token code-snippet code keyword">`entity_type_id`</span> = <span class="token code-snippet code keyword">`node`</span>, <span class="token code-snippet code keyword">`bundle`</span> = requested type — the single source of truth for which fields exist, which are required, and what format each expects. If the bundle is unknown, <span class="token code-snippet code keyword">`entity_type_list`</span> can enumerate bundles; if still unresolved, return <span class="token code-snippet code keyword">`blocked: need the content type`</span>.
<span class="token list punctuation">2.</span> <span class="token code-snippet code keyword">`entity_stub`</span> for <span class="token code-snippet code keyword">`node`</span> + bundle, with the user-provided title in <span class="token code-snippet code keyword">`base_fields`</span>. Set <span class="token code-snippet code keyword">`status`</span> to published.
<span class="token list punctuation">3.</span> For each field, <span class="token code-snippet code keyword">`field_set_value`</span> (entity object from step 2, machine <span class="token code-snippet code keyword">`field_name`</span>, value in the expected format). Only set fields that exist for the bundle. Generate suitable HTML body content (not Markdown) when the user asked for content about a topic but gave no body. If a required field is missing and cannot be reasonably generated, return <span class="token code-snippet code keyword">`blocked: need &lt;field>`</span>.
<span class="token list punctuation">4.</span> <span class="token code-snippet code keyword">`entity_save`</span> with the final entity.
 
<span class="token title important"><span class="token punctuation">##</span> Edit workflow</span>
 
<span class="token list punctuation">1.</span> Load: <span class="token code-snippet code keyword">`entity_load_by_id`</span> when a node ID is given. If only a title is given, <span class="token code-snippet code keyword">`entity_load_by_property`</span> with property <span class="token code-snippet code keyword">`title`</span> — note this returns property maps, NOT entity artifacts, so read the matching ID and then re-load with <span class="token code-snippet code keyword">`entity_load_by_id`</span> to get a usable entity. If multiple match, return <span class="token code-snippet code keyword">`blocked: need which node — &lt;list id/title>`</span>.
<span class="token list punctuation">2.</span> <span class="token code-snippet code keyword">`entity_revision_add`</span> on the loaded entity — MANDATORY before changing fields, so the edit is a new revision.
<span class="token list punctuation">3.</span> <span class="token code-snippet code keyword">`entity_field_value_definitions`</span> for the resolved bundle (use <span class="token code-snippet code keyword">`entity_metadata`</span> first if the bundle is unknown).
<span class="token list punctuation">4.</span> <span class="token code-snippet code keyword">`field_set_value`</span> for each field the user wants changed; reuse the existing valid text format, never guess a new one. Do not touch fields the user did not mention unless required for a valid save.
<span class="token list punctuation">5.</span> <span class="token code-snippet code keyword">`entity_save`</span>.
 
<span class="token title important"><span class="token punctuation">##</span> Graceful failure</span>
 
If any tool fails, stop, return the tool error output as-is, and offer up to three concrete next steps (confirm bundle, provide required field, provide node ID, check permissions). Never retry the same failing call with the same inputs.
 
<span class="token title important"><span class="token punctuation">##</span> Output contract (to the Orchestrator, not the user)</span>
 
<span class="token list punctuation">*</span> Success (create): the numeric node ID (state it as <span class="token code-snippet code keyword">`node ID &lt;N>`</span> so the Orchestrator can build <span class="token code-snippet code keyword">`/node/&lt;N>`</span>), bundle, title, and the saved status.
<span class="token list punctuation">*</span> Success (edit): the numeric node ID (as <span class="token code-snippet code keyword">`node ID &lt;N>`</span>) and a short list of fields changed.
<span class="token list punctuation">*</span> Blocked: a single <span class="token code-snippet code keyword">`blocked: need &lt;X>`</span> line carrying any options the Orchestrator will need (e.g. the list of matching nodes).
 
Do not add preview links, workspace labels, or edit links — the Orchestrator assembles the user-facing reply.</code></pre>
<h3>Workspace Agent Prompt</h3>
<p>This agent uses tools like <code>tool:tool_belt:workspace_create_and_switch</code> and <code>tool:tool_belt:workspace_preview_link</code>.</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">##</span> Role</span>
 
You are the Workspace Agent for a Drupal 11 AI Site. You report to the Orchestrator, never directly to the user.
 
You handle only workspace operations and workspace lifecycle actions. You do not create or edit content entities such as nodes or media. If asked for content work, return <span class="token code-snippet code keyword">`blocked: content operations must be routed to the Content Agent`</span>.
 
<span class="token title important"><span class="token punctuation">##</span> Tool execution rule</span>
 
Use the available workspace tools to perform actions; do not describe what you would do. Do not call tools speculatively.
 
Every invocation is INDEPENDENT and STATELESS. Decide what to do from THIS instruction alone — never from what an earlier invocation did, and never carry over a plan from a previous call. The Orchestrator may call you several times in one task (e.g. setup, then later preview); each call is a fresh, self-contained request.
 
Classify the instruction into exactly ONE action, then call ONLY the tools that action's allowed set permits (see the Allowed tools per action table). Any tool NOT in that set is forbidden for this invocation — do not call it for context, convenience, or to "finish the job." Calling a tool outside the action's set is the primary failure mode: it produces wrong state and can be fatal (e.g. creating or switching a workspace during a preview or publish call).
 
If the instruction is ambiguous or names no recognizable action, return <span class="token code-snippet code keyword">`blocked: need &lt;what is unclear>`</span> — never guess by calling a mutation tool.
 
<span class="token title important"><span class="token punctuation">##</span> Allowed tools per action</span>
 
For each action, call ONLY the tools listed. <span class="token code-snippet code keyword">`workspace_exists`</span> is additionally allowed with ANY action solely to resolve a label to an ID (Label resolution). Nothing else is ever implied.
 
<span class="token table"><span class="token table-header-row"><span class="token punctuation">|</span><span class="token table-header important"> Action (what the instruction asks for) </span><span class="token punctuation">|</span><span class="token table-header important"> Tools you may call — and NO others </span><span class="token punctuation">|</span>
</span><span class="token table-line"><span class="token punctuation">|</span> <span class="token punctuation">---</span> <span class="token punctuation">|</span> <span class="token punctuation">---</span> <span class="token punctuation">|</span>
</span><span class="token table-data-rows"><span class="token punctuation">|</span><span class="token table-data"> Setup / establish workspace for staging </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_get_active`</span>, then <span class="token code-snippet code keyword">`workspace_create_and_switch`</span> (only per the stage rules below) </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> Preview link </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_preview_link`</span> ONLY. Never create, switch, or check active. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> Publish </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_tracked_content`</span>, then <span class="token code-snippet code keyword">`workspace_publish`</span>. Never create or switch. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> Revert </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_revert`</span> ONLY. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> Delete </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_delete`</span> — plus <span class="token code-snippet code keyword">`workspace_switch`</span> (to Live) FIRST only if deleting the active workspace. Never create. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> Switch </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_switch`</span> ONLY. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> Read-only check (active/exists) </span><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`workspace_get_active`</span> and/or <span class="token code-snippet code keyword">`workspace_exists`</span> ONLY. Never mutate. </span><span class="token punctuation">|</span>
</span></span> 
<span class="token title important"><span class="token punctuation">##</span> Workspace tools reference</span>
 
<span class="token table"><span class="token table-header-row"><span class="token punctuation">|</span><span class="token table-header important"> Tool </span><span class="token punctuation">|</span><span class="token table-header important"> When to use </span><span class="token punctuation">|</span>
</span><span class="token table-line"><span class="token punctuation">|</span> <span class="token punctuation">---</span> <span class="token punctuation">|</span> <span class="token punctuation">---</span> <span class="token punctuation">|</span>
</span><span class="token table-data-rows"><span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_get_active`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Report the current workspace. Returns <span class="token code-snippet code keyword">`has_active`</span>, <span class="token code-snippet code keyword">`workspace`</span>, <span class="token code-snippet code keyword">`is_live`</span>. Being on Live is a normal success state. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_exists`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Resolve a label to an ID, or verify a workspace exists. Labels are not unique; the first exact match wins. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Create a new workspace and switch to it in one call. The canonical way to start staging. During staging setup, call this only when Live is active or the user explicitly asked for a new workspace — otherwise reuse the already-active workspace. Works regardless of the currently active workspace (no need to switch to Live first); <span class="token code-snippet code keyword">`parent`</span> accepts a parent workspace ID only; the active workspace is never implicitly the parent. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_switch`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Switch to an existing workspace by ID. Omit the <span class="token code-snippet code keyword">`workspace`</span> input to switch to Live. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_tracked_content`</span> </span><span class="token punctuation">|</span><span class="token table-data"> List what changed in a workspace. Use before publishing. Defaults to the active workspace when the input is omitted. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_publish`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Publish all tracked content in a workspace to Live. Defaults to the active workspace when the input is omitted. Only top-level workspaces can be published. Destructive. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_revert`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Revert a previously published (closed) workspace. Requires the WSE module. Destructive. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_preview_link`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Generate a shareable preview link. Default expiry is 8 hours; pass <span class="token code-snippet code keyword">`expiry`</span> only when the user requested a specific duration. Pass <span class="token code-snippet code keyword">`redirect_url`</span> (an internal path such as <span class="token code-snippet code keyword">`/node/&lt;ID>`</span>) to make the preview open directly on that page rather than the workspace root. The URL is a bearer capability — anyone holding it can view the workspace. </span><span class="token punctuation">|</span>
<span class="token punctuation">|</span><span class="token table-data"> <span class="token code-snippet code keyword">`tool_belt:workspace_delete`</span> </span><span class="token punctuation">|</span><span class="token table-data"> Delete a non-active workspace. Destructive. </span><span class="token punctuation">|</span>
</span></span> 
<span class="token title important"><span class="token punctuation">##</span> Mode behavior</span>
 
This section is the SETUP action only — it applies solely when the instruction asks you to establish the workspace context for staging. It NEVER applies to a preview, publish, revert, delete, switch, or read-only instruction; for those, ignore this section entirely and follow only that action's row in the Allowed tools table and its Action flow. A preview instruction arriving after setup is a preview action, not setup — do not re-run any of the steps below.
 
The Orchestrator passes one task mode: <span class="token code-snippet code keyword">`live`</span> or <span class="token code-snippet code keyword">`stage`</span>. Never choose or change the mode yourself.
 
<span class="token title important"><span class="token punctuation">###</span> stage</span>
 
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_get_active`</span> first — its result decides what follows
<span class="token list punctuation">*</span> If a non-live workspace is already active: use it. Do NOT call <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span>. Do not switch. Return its label and ID and state clearly that it was reused, not created.
<span class="token list punctuation">*</span> Only if Live is active: call <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span> with the provided label, or derive a short label from intent such as <span class="token code-snippet code keyword">`Draft &lt;bundle> &lt;short title>`</span> or <span class="token code-snippet code keyword">`Update &lt;bundle> &lt;short title>`</span>. Return the new label and ID and state clearly that the workspace was newly created.
<span class="token list punctuation">*</span> Exception: if the instruction states the user explicitly asked for a new workspace, call <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span> even while another workspace is active — do not switch to Live first; creation does not depend on the active workspace
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span> at most once per invocation. If it fails, verify the rollback (see below) and then return a <span class="token code-snippet code keyword">`blocked:`</span> or failure result. Never call <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span> a second time in the same run — not with a different label, not after any rollback check. A failed create is terminal for this run.
<span class="token list punctuation">*</span> If the create call fails: call <span class="token code-snippet code keyword">`tool_belt:workspace_exists`</span> with the label to verify the rollback actually removed it (the failure message "The workspace was not created." can be false); report an orphan workspace if one is found
<span class="token list punctuation">*</span> Setup establishes the workspace only. Do NOT generate a preview link during setup — the preview is a separate, later instruction (issued once the content exists so it can redirect to the new page). Return the workspace label and ID.
 
<span class="token title important"><span class="token punctuation">###</span> live</span>
 
<span class="token list punctuation">*</span> Do not create or reuse a workspace. Perform read-only checks only if the Orchestrator asks for them.
 
<span class="token title important"><span class="token punctuation">##</span> Label resolution</span>
 
When given only a workspace label, resolve it first: one <span class="token code-snippet code keyword">`tool_belt:workspace_exists`</span> call converts the label to an ID (labels are not unique; the first exact match wins). If it returns <span class="token code-snippet code keyword">`exists = false`</span>, return <span class="token code-snippet code keyword">`blocked: need the workspace ID — label "&lt;label>" did not resolve`</span>. Prefer IDs everywhere and always capture the ID from tool results.
 
<span class="token title important"><span class="token punctuation">##</span> Action flows</span>
 
<span class="token title important"><span class="token punctuation">###</span> Switch</span>
 
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_switch`</span> with the workspace ID (resolve a label first, per Label resolution)
<span class="token list punctuation">*</span> To return to Live: call <span class="token code-snippet code keyword">`tool_belt:workspace_switch`</span> with the <span class="token code-snippet code keyword">`workspace`</span> input omitted
 
<span class="token title important"><span class="token punctuation">###</span> Publish (explicit request only)</span>
 
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_tracked_content`</span> for the target (omit <span class="token code-snippet code keyword">`workspace`</span> for the active one). If there are no tracked changes, do not publish and report that there is nothing to publish.
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_publish`</span>
<span class="token list punctuation">*</span> If it fails because the workspace has a parent, report that only top-level workspaces can be published
 
<span class="token title important"><span class="token punctuation">###</span> Revert (explicit request only)</span>
 
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_revert`</span> directly (it reports not-found with a friendly message)
<span class="token list punctuation">*</span> On failure, explain that revert requires the WSE module and a previously published (closed) workspace, and include the tool error output
 
<span class="token title important"><span class="token punctuation">###</span> Preview link (explicit request only)</span>
 
<span class="token list punctuation">*</span> Call <span class="token code-snippet code keyword">`tool_belt:workspace_preview_link`</span> directly (omit <span class="token code-snippet code keyword">`workspace`</span> for the active one). No confirmation round-trip; state the expiry in the result.
<span class="token list punctuation">*</span> If the instruction names a target page or path (e.g. <span class="token code-snippet code keyword">`/node/&lt;ID>`</span>), pass it as the <span class="token code-snippet code keyword">`redirect_url`</span> input so the preview opens directly on that page instead of the workspace root. Accept an internal path only; if given a bare node ID, format it as <span class="token code-snippet code keyword">`/node/&lt;ID>`</span>.
<span class="token list punctuation">*</span> Return the <span class="token code-snippet code keyword">`preview_url`</span> verbatim as plain text (see Output contract → Preview). Do not wrap it in markdown or an HTML anchor — just the raw URL.
<span class="token list punctuation">*</span> If it fails because of a missing module, explain that the WSE Preview module is required
 
<span class="token title important"><span class="token punctuation">###</span> Delete (explicit request only)</span>
 
<span class="token list punctuation">*</span> If the target is the active workspace: first call <span class="token code-snippet code keyword">`tool_belt:workspace_switch`</span> with the input omitted (switch to Live), then <span class="token code-snippet code keyword">`tool_belt:workspace_delete`</span>
<span class="token list punctuation">*</span> Otherwise call <span class="token code-snippet code keyword">`tool_belt:workspace_delete`</span> directly
 
<span class="token title important"><span class="token punctuation">##</span> Error interpretation</span>
 
<span class="token list punctuation">*</span> <span class="token code-snippet code keyword">`Tool plugin access denied.`</span> from switch, publish, delete, preview, or tracked-content ALSO means "workspace not found" — re-check the ID before reporting a permission problem
<span class="token list punctuation">*</span> Branch on message content, never on success status alone: "No workspace found ...", "has no changes to publish", and <span class="token code-snippet code keyword">`exists = false`</span> are informative results, not errors to retry
<span class="token list punctuation">*</span> Never retry a failed mutation with the same or different inputs; in particular a failed <span class="token code-snippet code keyword">`tool_belt:workspace_create_and_switch`</span> is terminal for this run — do not attempt it again with any label
 
<span class="token title important"><span class="token punctuation">##</span> Output contract</span>
 
<span class="token title important"><span class="token punctuation">###</span> Success</span>
 
<span class="token list punctuation">*</span> Workspace actions: the workspace label and ID, plus what happened (created, reused, switched, published, reverted, or deleted)
<span class="token list punctuation">*</span> Preview: as the FIRST line, the exact <span class="token code-snippet code keyword">`preview_url`</span> string from the tool result, as plain text on its own line — copied verbatim, with no surrounding markup, no markdown link, and no HTML anchor. Follow it with the expiry on the next line. Do not include the workspace label or ID, node links, or edit links in a preview response. If you did not receive a <span class="token code-snippet code keyword">`preview_url`</span>, return the failure instead.
 
<span class="token title important"><span class="token punctuation">###</span> Blocked</span>
 
<span class="token list punctuation">*</span> One line: <span class="token code-snippet code keyword">`blocked: need &lt;what is missing>`</span> (for example the workspace ID for an unresolved label, or which workspace to publish). The Orchestrator owns all user dialogue.</code></pre>
<h2>Why This Matters for Site Builders</h2>
<p>Tool Belt is not just about adding capabilities, it is about controlling them.</p>
<p>With the right setup, you can ensure that:</p>
<ul>
<li>Content is staged by default</li>
<li>Preview links are always generated</li>
<li>Publishing requires review</li>
<li>Destructive actions require confirmation</li>
<li>Agents only have access to the tools they need
This makes AI workflows safer, more predictable, and easier to manage.</li>
</ul>
<h2>Final Thoughts</h2>
<p>Tool API provides the foundation. Tool Belt provides the tools. Workspaces provide a safe environment.</p>
<p>Together, they enable AI-assisted workflows that respect Drupal's editorial process while improving efficiency.</p>
<p>The result is not just smarter automation, but governed automation, where AI helps and humans stay in control.</p>
<p>Bringing governance to AI-driven changes is <a href="https://www.tag1.com/blog/tag1-joins-drupal-ai-initiative/">Tag1's starting contribution</a> to the Drupal AI Initiative, and this workspace tooling is where it begins.</p>
]]></description>
      <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Ajit Shinde</dc:creator>
      <guid>https://www.tag1.com/blog/safer-ai-workflows-with-toolbelt-workspaces/</guid>
    </item>
    
    
    <item>
      <title>Teaching AI to Speed Up Accessibility Testing</title>
      <link>https://www.tag1.com/blog/teaching-ai-to-do-the-boring-parts/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
         <a href="https://www.tag1.com/team/#marlene-wanberg">Marlene Wanberg</a>, Frontend Developer at Tag1 and a Drupal builder since 2007, built a suite of fifteen AI agent skills that runs a full automated accessibility audit and narrows thousands of raw scanner findings down to a short worklist of confirmed fixes. 
    </div>
</div>
</div>
<p>To paraphrase the much used line about writing: I don't enjoy doing accessibility testing, but I like having done accessibility testing. When a site becomes more usable for everyone, I feel good. Users feel good. Clients feel good. (Regulators feel good.) The process of getting there can be very tedious.</p>
<p>Before we even begin addressing accessibility, we need to know where the problems live, and that's where testing comes in. There are two main categories of accessibility testing: automated and manual. Automated testing uses deterministic tools and scanning scripts to check the rendered HTML markup for certain obvious flaws, things like missing ARIA labels or low color contrast. Manual testing checks what those tools can't. A person navigates the site with the tech real users rely on, like keyboards and screen readers, and catches holes in user flows or spots where meaning gets missed. An image might have text in its ARIA label, but does that text actually help explain what the image is and how it's relevant to the rest of the content? Both categories let us find the areas of a site that need adjustments to make them more usable.</p>
<p>People are having plenty of thoughtful arguments right now about what AI is good for and where it doesn't belong. One use stands out to me: letting it take on the repetitive, mechanical parts of a job, freeing me up for the work that needs real judgment. So I set out to see how much of accessibility testing an AI agent could carry.</p>
<p>Automated testing seemed like the most logical place to start, because so much of it is deterministic: you run the scanners and collect the results. And yet a full automated pass involves lots of little decisions along the way, lots of setup steps, documentation to keep straight, mountains of results to sort through, and reports to write at the end. Exactly the type of work I wanted to hand off.</p>
<h2>One Skill File Was Not Nearly Enough</h2>
<p>Claude Code has a feature called skills: instruction files that teach the agent a repeatable procedure. That seemed like a promising starting point. There might be other approaches that smarter people have thought of, but this is what I knew at the time, and one of the best ways to learn is to just do and experiment. So I had a conversation with Claude and had it help me write my first automated accessibility testing skill. (In my experience, having AI write instructions for AI based on my intent tends to give me better results.)</p>
<p>I very quickly realized that one skill file could not handle the complexity I was asking of it. So I split it up: a skill for gathering information about the project, one for setup, one for running the tools, one for consolidating and analyzing the results, one for tracing issues back to their source, and one for reporting.</p>
<p>The suite kept growing as I used it and learned what else it needed to be robust. I'd run it through fresh on a site and each time find different ways it either wasn't doing enough or was just doing it flat wrong. Instead of getting mad (ok, I did get annoyed a few times) I asked, &quot;Where is my process breaking down? What does the agent need that I haven't provided? Where is it spending the most tokens and how can I make that more efficient?&quot;</p>
<p>The process now spans fifteen skills and a shared library of tested scripts, covering the full arc of an audit: plan the scope, discover and categorize the pages, pick and configure the scanners, run them, boil the output down, trace findings to source code, verify what's real, research fixes against the actual specs, write reports for the humans who need them, and retest after fixes get implemented.</p>
<p>I also wanted the workflow to work on any project, not just the stacks I know best. Drupal, Next.js, Svelte, WordPress, whatever comes through the door. Including up-to-date documentation for every likely framework inside the skills would have been unrealistic, and stale guidance is worse than no guidance, because the agent follows it confidently. (Just ask any Drupal dev trying to use AI out of the box for dev help). So I baked in a dependency on Context7, a service that lets the agent query current, version-specific documentation for whatever the project uses, and required checking it at several points in the workflow. Now it works from what the project is actually running instead of trusting whatever its training data half-remembers.</p>
<h2>Humans Stay In the Loop, On Purpose</h2>
<p>I value ownership of my work. Current models can do a lot, but they still far too often make inaccurate inferences, skip facts, and end up like my robo-vacuum, stuck in a corner and tangled in cords, costing me time and effort to get it unstuck and redo work. In personal projects this is annoying; in a regulatory environment this is unacceptable.</p>
<p>So at key moments I want my agents to bring me their work, get my input and signoff, and then continue based on the direction I set. The agent interviews me up front about the project and its goals. It asks permission before installing tooling dependencies or guides me through the installs. It pauses so I can check that the process and the results so far look accurate, and that it hasn't wandered off track.</p>
<h2>What Broke Along the Way</h2>
<p>Many useful things I learned in this process came from something going wrong. A sampling:</p>
<p><strong>False positives.</strong> One scanner rule alone produced 257 rows complaining that icon-only buttons (picture a bare magnifying-glass search button) had no label for screen readers, when in reality every one of them was labeled correctly. A quick fix might be to tell the scanner to stop running that rule, but a genuinely unlabeled icon button elsewhere in the site is a real barrier for someone navigating by screen reader, so switching it off entirely would bury real problems alongside the noise. Instead the workflow keeps a list of these known false-alarm patterns, each with a condition attached: ignore this rule only where the evidence proves a label already exists. And it keeps paired test cases, one that should trip the rule and one that shouldn't. If the exception ever starts covering a real failure, a test catches it.</p>
<p><strong>Too much output, then not enough.</strong> Four scanners across a real site produced 188 raw result files holding over three thousand findings, most of them duplicates of each other in different formats. I had to build a whole consolidation stage: normalize, deduplicate, cluster by root cause, rank. Then I discovered the agent was dropping and misclassifying findings during consolidation, so I pushed that work out of the agent's judgment and into tested scripts whose behavior I could verify.</p>
<p><strong>Empty, meaningless reports.</strong> Early reports read like typical AI marketing copy, generic percentages and process jargon instead of actual numbers from the audits and explanations a developer could act on. They were unusable to me, the person who had instructed the AI to write them, and would certainly not be usable to anyone else. Now every report has to be built from the audit's actual findings, and written for the specific person who will read it.</p>
<p><strong>The disappearing CSV parser.</strong> I watched the agent spend enormous amounts of tokens recreating, over and over, a CSV parser I knew it had already written, until I cornered it on why. The library it needed had gone missing mid-session, and rather than say so, it kept quietly rebuilding the wheel. The lesson went straight into the shared library's principles: “Never write a one-off parser; if the library doesn't support what you need, first check in with me, then extend the library and add tests.”</p>
<p><strong>Confidently wrong.</strong> Authentication tripped it in a way I almost didn't catch. After one scan cleared browser cookies, the session cookie never got reapplied, so every authenticated page silently scanned as the login page and came back artificially clean. Clean results feel great until you notice the settings page weighs a fraction of what it should. And color contrast in modern CSS gave it fits; converting <code>oklch()</code> color values to check contrast ratios burned real time and produced confident errors before I required the math to live in a tested script rather than the agent's head.</p>
<p>Whenever something like this came up, I stopped the agent and we talked through what happened and why. Then I changed the process and the skills themselves: more decomposition, more ask-the-human steps, more reusable tested scripts, and adversarial review agents that critique the work before I see it. Each time the pattern is the same: treat agent failures as process bugs, not one-offs to scold away.</p>
<h2>Did It Produce Anything?</h2>
<p>Yes, it did, and I'm quite pleased with the results.</p>
<p>Take a static marketing site. The workflow pointed four accessibility scanners (axe, Pa11y, Lighthouse, and IBM Equal Access) at 35 pages. Between them they returned 7,028 raw findings. Most automated scanners finish by handing me a pile of output that I have to sort through to find the meaning. That's boring and annoying so I use my workflow to remove duplicates, group the remaining items by root cause, and in this case it landed on 37 clusters. A cluster is just a bunch of findings that all come from the same underlying problem, so one broken pattern repeated across fifty pages becomes a single cluster instead of fifty separate things to chase. The review step of the workflow turns those into a short worklist. 11 of them are confirmed code fixes ready to act on. 7k+ findings down to 11 things I need to do.</p>
<p>I've run it on four sites so far, on stacks that share almost nothing, and gotten similar results each time.</p>
<table>
<thead>
<tr>
<th>Site</th>
<th>Raw findings</th>
<th>Narrowed to</th>
</tr>
</thead>
<tbody>
<tr>
<td>Eleventy site</td>
<td>7,028</td>
<td>37 clusters</td>
</tr>
<tr>
<td>Drupal module</td>
<td>1,091</td>
<td>24 clusters</td>
</tr>
<tr>
<td>Next.js app</td>
<td>3,216</td>
<td>32 clusters</td>
</tr>
<tr>
<td>SvelteKit site</td>
<td>6,980</td>
<td>26 clusters</td>
</tr>
</tbody>
</table>
<p>And if I had questions about any of them, like where a problem came from, which WCAG rule it violated, even a suggested fix, I could talk it through with the agent, which had the full context of the project.</p>
<blockquote>
<p>One of my favorite bits is how the workflow traces big noisy messes back to the source. On the Eleventy site, it found that fixing just two files (one layout template and one stylesheet) would clear 82% of its 7,028 findings. That is exactly the kind of combing-through I used to do by hand, and exactly what AI is good at.</p>
</blockquote>
<div class="">
<p>It is not only good at ruling things out or narrowing down issues. I've found it is good at bringing up and prioritizing problems that matter but that wouldn't necessarily be highlighted using just the automated scanners. For example, on a Next.js app it confirmed a link set apart by color alone, a genuine problem for anyone who cannot see the difference. On a SvelteKit site it flagged something styled as a button that was really a plain span, invisible to a keyboard. In the Drupal LMS module, it found the course card was two overlapping links pointing to the same place, which adds noise to a screen reader. And it found where code blocks failed contrast in dark mode, a problem a quick once-over would sail right past.</p>
<h2>How Do I Know It Isn't Missing Things?</h2>
<p>If my workflow drops the findings from thousands to under a hundred actionable items, that can feel good, but it also raises the question: Are we missing something now?</p>
<p>Two things stop that from happening. First, the automated pass is not the full audit. Scanners cover the slice of WCAG a machine can check, which is a portion of what matters. The rest still needs a person at the keyboard with assistive technology. For example, on the Drupal module, my own manual review added seventeen findings no scanner could have caught, no matter how many times I ran them. Second, someone who knows what &quot;wrong&quot; looks like has to read the workflow's output. Those authenticated pages that scanned clean because the session cookie dropped? A less experienced reviewer might file that clean result and move on. The workflow speeds up an expert. It does not replace one.</p>
<h2>The Workflow Keeps Evolving</h2>
<p>I treat the workflow itself as a project under audit. Each time I use it I find ways to improve it. This summer I handed a newer, more capable model a bigger job: review the entire suite and plan a remediation of its rough edges, from turning the copy-paste method I'd been using to move it between projects into a proper plugin to addressing weak spots in the consolidation stage.</p>
<p>That overhaul is underway now. The intake interviews will be better, I'm incorporating the latest WCAG Evaluation Methodology, and I'm experimenting with AI-driven keyboard testing.</p>
<h2>What You Can Take From This</h2>
<p>I haven't released this workflow yet; it remains a personal tool that I use on the projects in front of me, including Tag1's. But the pattern is the transferable part, and none of it requires my code:</p>
<ul>
<li>Decompose the work until each piece has one job.</li>
<li>Keep a human at the decisions that matter.</li>
<li>Push the agent toward current documentation for the project's actual stack instead of letting it coast on training data.</li>
<li>Fix the process when the agent fails.</li>
<li>Move anything deterministic out of the agent's judgment and into tested scripts.</li>
<li>Measure and review before you trust; sometimes the AI's most confident conclusions turned out to be based on broken steps.</li>
<li>Add adversarial agent reviews to both the planning and the workflow.</li>
</ul>
<p>None of this made accessibility testing fully automatic, and that wasn't my goal. It has made many of the tedious parts quick, and I get to spend more of my time doing the interesting parts now.</p>
<p>This is one of several ways we're putting AI to work on real engineering problems at Tag1. You can find more in our <a href="https://www.tag1.com/insights/" target="_blank" rel="noopener noreferrer">Insights</a>.</p>
</div>
]]></description>
      <pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Marlene Wanberg</dc:creator>
      <guid>https://www.tag1.com/blog/teaching-ai-to-do-the-boring-parts/</guid>
    </item>
    
    
    <item>
      <title>Ten Minutes, Not an Hour: What Efficient AI-Assisted Development Actually Looks Like</title>
      <link>https://www.tag1.com/blog/what-ai-assisted-development-looks-like/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
        <a href="https://www.tag1.com/team/#marcin-grabias">Marcin Grabias</a>, Senior Drupal Engineer and maintainer of the Drupal LMS module, used Claude Code to merge two near-duplicate activity plugins into a single configurable one in roughly ten minutes of work that would have taken an hour by hand.
    </div>
</div>
</div>
<p>The <a href="https://www.drupal.org/project/lms" target="_blank">Drupal LMS module</a> lets site builders define quiz-style &quot;activities&quot;: question types a learner answers as part of a course. For a while, LMS shipped two separate plugins for selection-based questions: one for single-choice answers, one for multiple-choice. They did almost the same thing internally. The only real difference was the configuration toggle that didn't need to live in two separate classes once <a href="https://www.drupal.org/project/lms" target="_blank">Activity - Answer plugins</a> became configurable.</p>
<p><a href="https://www.drupal.org/project/lms/issues/3546362" target="_blank">Issue #3546362</a> merges the two plugins into a single configurable one, writes an update hook so existing sites migrate their configuration automatically without breaking, and updates the QA fixtures and functional tests that reference the old plugin pair. None of this is conceptually hard. Every long-lived module accumulates this kind of work, where touching four or five files consistently is non-negotiable and a missed reference breaks an update path for every site running the module.</p>
<p>That combination, low conceptual difficulty, high mechanical thoroughness, turns out to be exactly where an AI coding tool earns its keep.</p>
<h2>The Workflow: Solve It. Then Let the Prompt Become the Issue.</h2>
<p>As the module's maintainer, I didn't start by filing an issue and waiting for someone to pick it up. I solved it myself, with <a href="https://claude.com/claude-code" target="_blank">Claude Code</a> doing the implementation.</p>
<ol>
<li>I wrote a detailed prompt describing the merge: which two plugin classes to combine, what the resulting configuration should look like, what the update hook needed to handle for existing activity type entities, and which test fixtures and functional tests needed updating.</li>
<li>Claude Code implemented the merge, the update hook, and the test updates in one pass.</li>
<li>I reviewed the diff and made two or three rounds of corrections (nothing structural, mostly coding-standards and best-practice nits).</li>
<li>Once the change was solid, I opened the <a href="https://git.drupalcode.org/project/lms/-/merge_requests/117" target="_blank">merge request</a> and filed the <a href="http://drupal.org/">drupal.org</a> issue, using the same prompt, lightly trimmed, as the issue's Problem/Motivation and Proposed resolution text.</li>
</ol>
<p>That last step is worth sitting with. The prompt wasn't a throwaway instruction I deleted once the code worked. It was specific and complete enough that it doubled as the project documentation other contributors would read. Writing a good prompt and writing a good issue summary turned out to be the same task, done once.</p>
<h2>The Math: Ten Minutes vs. an Hour</h2>
<p>The ten-minute number assumes one more thing, though: a codebase that's giving the agent good examples to work from. Start to finish, including my review and correction passes, this took about ten minutes. Finding every reference to the two old plugin classes, writing the update hook, regenerating the QA fixtures, and adjusting the functional tests by hand would have taken me roughly an hour.</p>
<p>Claude Code isn't faster here because it's &quot;smarter.&quot; Repeatable, multi-file, consistency-dependent work is exactly what thoroughness-by-checklist is good at. Checking every reference to a renamed class across five files is something an agent handles in seconds without losing its place, while a human doing the same task fights boredom and the risk of missing the one reference buried in a test fixture. The hour I'd have spent wasn't an hour of hard thinking. It was an hour of careful, repetitive checking, which is the part of the job that's safe to delegate, provided someone still reviews the result.</p>
<h2>The Codebase Is Part of the Prompt</h2>
<p>It's easy to focus on the prompt and forget the other half of the equation. Claude Code is pattern-matching against whatever code already surrounds the change. A detailed prompt tells it what to build; the existing codebase tells it how things are built here. If that codebase is inconsistent, or full of workarounds and dead patterns, the agent will happily extend the inconsistency, with no way to know that the surrounding code is something to avoid imitating.</p>
<p>The Drupal LMS module's plugin architecture is consistent and modern, including typed properties, constructor-based dependency injection, and configuration schemas that follow Drupal's own conventions throughout. That's exactly the kind of codebase an agent can extend correctly on the first attempt, because the pattern it's matching against is the pattern you actually want repeated. The corrections in this case were minor precisely because there wasn't a backlog of inconsistent legacy code for Claude Code to mistakenly treat as precedent. On a messier, older codebase, the same prompt would likely have needed more correction rounds, not because the agent got worse, but because it had worse examples to learn from in the surrounding files.</p>
<p>This cuts both ways for anyone evaluating how well AI tools will work on their own project. The return on a good prompt is capped by the quality of the code already there. Cleaning up codebase inconsistencies isn't just good practice anymore; it's also an investment in how well an AI agent will be able to work in that code afterward.</p>
<h2>What Went Wrong (Briefly) and How to Fix It</h2>
<p>Nothing in the first pass was structurally wrong. The corrections across those two or three iterations were about coding standards and Drupal-specific best practice, the kind of thing a thorough code reviewer would flag.</p>
<p>The interesting part isn't that there were corrections; it's what happens to them afterward. Rather than re-explain the same coding-standard preference every time it comes up, I keep a running set of project conventions in <code>CLAUDE.md</code>, the instructions file Claude Code reads at the start of a session. Something like:</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">##</span> Coding Conventions</span>
 
<span class="token list punctuation">-</span> Use typed properties and constructor property promotion where the
  module's minimum PHP version allows it.
<span class="token list punctuation">-</span> Plugin classes depending on services must use dependency injection
  via <span class="token code-snippet code keyword">`create()`</span>, never <span class="token code-snippet code keyword">`\Drupal::service()`</span> calls inside plugin logic.
<span class="token list punctuation">-</span> Update hooks must be idempotent — check the current state before
  mutating config, since update hooks can be re-run in some workflows.
<span class="token list punctuation">-</span> New configurable plugins need a corresponding entry in
  <span class="token code-snippet code keyword">`tests/data/activity_types.yml`</span> before functional tests are updated.</code></pre>
<p>Every correction I make more than once is a candidate for this file. It's a small bit of overhead the first time, and it means the next plugin merge, or the next contributor using Claude Code on this codebase, doesn't relitigate the same coding-standards conversation. The conventions compound; the corrections don't repeat.</p>
<h2>Why I Don't Hand Off to Multiple Sessions</h2>
<p>It's tempting to treat this kind of repeatable work as something you can queue up and walk away from. Kick off a few sessions, come back when they're done. I don't do that, and I don't think it's the right tradeoff for code quality.</p>
<p>Every session I'm not actively reviewing is a session where Claude Code is making judgment calls without my insight in the loop. Spread across multiple unsupervised sessions, two things happen. The result gets less reliable, because small wrong assumptions compound instead of getting caught at step two; the cost goes up, because more back-and-forth is needed to recover from those assumptions than would have been needed to just confirm them with me directly. A single session where I review every proposed change as it's made costs more of my attention up front, but it costs less overall, and it's the only version of this where I can say with confidence that the result is correct. Not &quot;probably correct, I'll find out in code review.&quot;</p>
<p>That's the actual efficiency claim here, and it's worth being precise about it: the time saved comes from delegating mechanical thoroughness, not judgment. The ten minutes still include me reviewing every change.</p>
<h2>What Generalizes</h2>
<p>This was a small fix to a Drupal module, but the pattern holds for AI-assisted work generally:</p>
<ul>
<li><strong>A detailed prompt is documentation, not scaffolding.</strong> If you write it with enough care to drive a correct implementation, it's usually already good enough to be the issue, the PR description, or the changelog entry. Writing it twice is wasted effort.</li>
<li><strong>Mechanical thoroughness is the right thing to delegate; judgment isn't.</strong> The hour this would have taken by hand was mostly careful checking, not hard decisions. That's the profile of a task where an agent saves real time without costing you quality.</li>
<li><strong>Live review beats batched review.</strong> Catching a coding-standards issue at the moment it's introduced is cheaper in time, tokens, and correctness than discovering it after several unsupervised sessions have built on top of it.</li>
<li><strong>Recurring corrections belong in a conventions file, not in your head.</strong> A <code>CLAUDE.md</code> (or equivalent instructions file) that accumulates project-specific standards turns &quot;I have to say this again&quot; into &quot;the agent already knows this.&quot;</li>
<li><strong>The codebase is part of the prompt.</strong> An agent extends whatever patterns already surround it. A clean, consistent codebase gets clean, consistent output on the first try; a messy one teaches the agent to be messy too.
None of this requires exotic tooling. It requires treating the prompt as a real artifact and treating review as something that happens during the work, not after it.</li>
</ul>
<p>If you're trying to figure out where AI genuinely speeds up your development workflow, and where it doesn't — <a href="https://www.tag1.com/contact/">we'd love to hear about your project</a>.</p>
]]></description>
      <pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Marcin Grabias</dc:creator>
      <guid>https://www.tag1.com/blog/what-ai-assisted-development-looks-like/</guid>
    </item>
    
    
    <item>
      <title>A New Direction for Authentication in Drupal Core</title>
      <link>https://www.tag1.com/blog/new-direction-authentication-in-drupal-core/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
         At Tag1, we believe in proving AI within our own work before recommending it to clients. This post is part of our <a href="https://www.tag1.com/insights/?tag=AI#selected" target="_blank">AI Applied</a> content series, where team members share real stories of how they're using Artificial Intelligence and the insights and lessons they learn along the way. Here, <a href="https://www.tag1.com/team/#lucas-hedding">Lucas Hedding</a>, Senior Backend Engineer & Migration Lead and Drupal core subsystem maintainer for authentication/authorization, used Claude to work through over 1,200 open issues in the Drupal auth/authZ issue queues and co-architect a new pluggable authentication system for Drupal core, without writing a single line of code.
    </div>
</div>
</div>
<p>When approaching AI, I've done so warily. Maybe it was because I was a skeptic, but my first endeavors were not glowing success stories. My first real attempt to kick the tires ended with me kicking AI to the curb and doing some regex and search/replace to finish what it started. I chalk it up to a mix of model maturity and, let's be honest, my own ill-directed uses.</p>
<p>But more recently I've been finding wins. I find AI very useful for writing test cases for test-driven development (TDD). It's also really good at troubleshooting. It takes a bug report, follows the code paths, and writes a failing test that reproduces the bug. When you solve the problem, you can be sure you have solved it. And more importantly, that it was even a problem in the first place.</p>
<h2>Putting AI to Work on a Real Problem</h2>
<p>It was at this point, I realized that AI might be able to help me with my Drupal Core maintainer duties. For those that don’t know, I maintain an insane number of contrib modules and am a core subsystem maintainer in 3 areas, namely migrate, image, and authentication/authorization. The last area of auth/authZ is in desperate need of modernization.</p>
<p>The planning issue requirements and roadmap are all open. The community design review hasn't started yet, so now is a good time to take a look: <a href="https://www.drupal.org/project/drupal/issues/3593328" target="_blank">[Plan] YAML-Based Pluggable Authentication Flow</a></p>
<p></p><figure class="component my-10 grid gap-4">
<a href="https://www.tag1.com/img/blog/authentication-flow.png" target="_blank">
<img class="mx-auto  border-[var(--border-color)] border-2" src="https://www.tag1.com/img/blog/authentication-flow.png" alt="Screenshot of the authentication flow issue on drupal.org." />
</a><p></p>
<p></p></figure><p></p>
<p>The first part of the problem for auth/authZ is that there aren't any core components in the core issue queue for the sub system. I have to look in a few module queues and the base system to find relevant issues. I filed this issue to establish a <a href="https://www.drupal.org/project/drupal/issues/3592795" target="_blank">dedicated auth/authZ component in the Drupal core queue</a>.</p>
<p></p><figure class="component my-10 grid gap-4">
<a href="https://www.tag1.com/img/blog/authorization-system.png" target="_blank">
<img class="mx-auto  border-[var(--border-color)] border-2" src="https://www.tag1.com/img/blog/authorization-system.png" alt="Screenshot of the authentication and authorization system component on drupal.org." />
</a><p></p>
<p></p></figure><p></p>
<p>To help me get my mind around the space, I had AI query all 1,200 issues in the module-based issue queues. Then it spun out from there to find referenced issues. I leveraged a local file cache of d.o issues so I didn’t have to hit the <a href="https://drupal.org/" target="_blank">drupal.org</a> API repeatedly as I was tweaking the discovery.</p>
<p>This demonstrates the first lesson I’ve learned with AI. It is really good at doing directed research and planning. But you need to give it guard rails. I had to tell it to add a cache. I had to tweak the issue filters. I had to think about what I wanted. AI wouldn’t think for me. But at the end, I had several hundred issues downloaded locally.</p>
<h2>Making Sense of 1,200 Issues</h2>
<p>Then came the next step. I asked AI to create a mind map using <a href="https://mermaid.live/" target="_blank">mermaid.live</a>. With over 1,000 issues, I didn’t want something that was too unmanageable. I picked a couple issues that seemed key to me and asked AI to give me a mind map with issues directly related to authentication (excluding authorization for the moment). That shrank things down to just a few hundred. But the large picture of categorized issues in a mind map started to tell a story.</p>
<p>The mind map story led me back to the planning phase again. This time I used <a href="https://github.com/bmad-code-org/BMAD-METHOD" target="_blank" rel="noopener">BMAD</a>, an AI methodology specifically structured to guide planning a task using AI. I fed it the pain points and asked it to look at some reference PHP and non-PHP authentication frameworks. It researched Laravel, Symfony, Drupal and Keycloak. At this point I had enough data to request it to write some pros/cons and possible pseudo implementations.</p>
<p>Somewhere in this whole process DrupalCon Chicago happened. Then a few weeks later MidCamp in Chicago happened. This gave me ready access to real people to bounce ideas off. They asked some really great questions. I fed these questions back into AI and refined the design even further.</p>
<p>Then more recently, I had the opportunity to speak at a Drupal meetup on Zoom. I took all the data I’d gathered, the mind maps, the design artifacts generated by BMAD and created a nice slideshow presentation. But the source data was from research provided by AI. The attendees at the meetup had even more feedback. I fed this feedback back into AI and now have a pretty defensible architecture for a new authentication system in Drupal core — the <a href="https://www.drupal.org/project/drupal/issues/3593328" target="_blank">YAML-Based Pluggable Authentication Flow</a> outlined in the planning issue. Broken down into phases with dependencies identified between phases of work.</p>
<h2>The Blank Sheet Problem</h2>
<p>We haven’t built the new system. No code has been written. But AI helped architect everything. I don’t think a human could parse that many hundreds of <a href="https://drupal.org/" target="_blank">drupal.org</a> issues, create a mind map, and build a new architecture without massive amounts of effort. AI is great at holding lots of nuggets of data in memory all at once. It is optimally designed to help with just such a task as I went through.</p>
<p>Time will tell if the architecture co-developed by AI proves useful. I do know it has helped with the “blank sheet of paper” syndrome. The feeling where you know you need to do something but don’t know where to start. You just sit there staring at the blank sheet of paper hoping for inspiration. Even if we entirely threw out the new architecture, we have something to start.</p>
<p>For those interested in the artifacts from this discovery, you can visit <a href="https://www.drupal.org/project/drupal/issues/3593328" target="_blank">https://www.drupal.org/project/drupal/issues/3593328</a>.</p>
]]></description>
      <pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Lucas Hedding</dc:creator>
      <guid>https://www.tag1.com/blog/new-direction-authentication-in-drupal-core/</guid>
    </item>
    
    
    <item>
      <title>Beyond Batch and Queue: Temporal integration with Drupal</title>
      <link>https://www.tag1.com/blog/temporal-integration-with-drupal/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
        <p>Batch and queue are fine until they aren't. Once you've hit their limits and if your site is large enough, and you will, Temporal is the answer. Crashes, retries, scaling across machines: handled, transparently, without touching your business logic. Here, <a href="https://www.tag1.com/team/#k%C3%A1roly-n%C3%A9gyesi">Károly Négyesi</a>, Edge Case Engineer walks through how Temporal integrates with Drupal and why it's the next step for sites that have outgrown what Drupal ships with.</p>
    </div>
</div>
</div>
<h2>Background</h2>
<p>Beyond serving pages, a lot of websites have background processes they need to run. Importing third party data, indexing changes, generating reports and so on. Once a site becomes so large it is no longer feasible for a single PHP process to do this for every entity it cares about, things become more difficult.</p>
<h2>What Drupal Offers</h2>
<p>Drupal provides two APIs for long-running processes: batch and queue. A batch runs an operation, saves state and then runs it again until the operation says &quot;stop&quot;. However, if an operation fails then the entire batch aborts. Queue lines up operations and runs them independently of each other so the error handling is somewhat better. If an operation fails then the rest of the operations can run but there is very little control over retries. These two are good for the basics, they are good enough to be included in Drupal core, but when you are dealing with larger sites they are woefully inadequate. I should know: I wrote the queue API originally.</p>
<p>What if we had an ability to run these processes without having to worry about crashes, retries, or scaling across machines with extensive reporting about what has happened? That system is <a href="https://temporal.io/" target="_blank" rel="noopener noreferrer">Temporal</a>, and this post will walk through how it integrates with Drupal.</p>
<h2>Temporal Basics</h2>
<p>In Temporal, your Drupal code is an activity. Activities are a single unit of work. It doesn't matter whether it takes a short or long time, for example transcoding media might take a long time, but it's still a single unit. A workflow tells Temporal which activities to run and how to retry them. It has a rich selection of retry strategies. It also controls various timeouts. One of the more interesting timeouts is the heartbeat for long-running activities: if the activity doesn't send a heartbeat within the specified time, it will be cancelled. Heartbeats can also carry progress information.</p>
<p>The workflow is a long-running process and if it stops for any reason, Temporal makes it resume where it stopped. When I first read this I thought &quot;huh, maybe it somehow saves the memory state but that'd be very fugly&quot; and no, that's not what it does. Instead, this magic is achieved by saving the inputs and outputs of every activity call in its own durable event log. There is a UI to see the log which also includes workflow events besides these activity events.</p>
<h2>The Magic</h2>
<p>Thanks to this event log, when a workflow is restarted the activities do not need to be rerun, the workflow fast-forwards to the point where it stopped based on the event log replay. I can't emphasize enough how important this is: no matter what crashes and when, the system completely transparently handles it. The activity calls a third party service that is temporarily down and so it needs to return with an error? PHP crashed with an out of memory error because Drupal leaks memory like a sieve? No need to worry about any of this, no need to write elaborate retry strategies for the remote call, no need to try to patch up the leaking sieve. Temporal will retry the activity, the workflow will continue and neither needs to care about errors and crashes.</p>
<h2>A Little Theory</h2>
<p>For this to work well, workflows need to be deterministic: given the same series of events, a workflow must always make the same decisions. For this reason, workflows should not consult databases, file systems, clocks, random numbers, and the like. That's a job for activities. Most workflows will even avoid logging to prevent any side effects, especially since Temporal already records all activity inputs and outputs.</p>
<p>And activities are recommended to be idempotent: running them multiple times should have the same result as running them once. This is important because they can be re-tried if they fail. A classic example of an idempotent operation is the stop button on a media player: no matter how many times you tap it the music will not play. The play/pause button, on the other hand, is the classic example for a non-idempotent operation. Within PHP, writing to a stream opened with <code>fopen('filename', 'w')</code> is idempotent: the contents of the file become the data written. On the other hand, if a stream is opened with <code>fopen('filename', 'a')</code> then the writes are not idempotent: the data is appended over and over again.</p>
<p>To further highlight the difference between the two, consider a database <code>MERGE</code>: the operation will want to report back whether the row was inserted or updated, so it is not deterministic, but it is idempotent because the database row ends up with the same data either way.</p>
<p>Enough of the theoretical talk, let's talk code!</p>
<h2>Coding a Workflow</h2>
<p>A workflow is a PHP class. To make it easy for Drupal to discover them, they are in the <code>Drupal\mymodule\Temporal\Workflow</code> namespace and the class has the <code>Temporal\Workflow\WorkflowInterface</code> attribute. This pattern should be familiar from writing plugins. This attribute is enough for Temporal to recognize this as a workflow class. Temporal also requires the workflow method to have the <code>Temporal\Workflow\WorkflowMethod</code> attribute.</p>
<p>While a workflow looks like a Drupal plugin, it is not. As we discussed workflows need to be deterministic and due to the complexity of Drupal it is almost impossible to guarantee any call into Drupal to be deterministic so it's best if workflows do not talk to Drupal at all. The integration encourages this: workflow classes are instantiated by Temporal directly without passing any arguments to the constructor.</p>
<p>The most important thing a workflow does is calling an activity. The Temporal PHP SDK's mechanism for this is a bit unusual at first, but it's the same pattern as mocks/stubs in phpunit: activity classes get a stub in the phpunit sense and the methods defined in the activity class are called on this stub.</p>
<p>For example:</p>
<pre class="language-php"><code class="language-php"><span class="token comment">/** @var \Drupal\temporal\Temporal\GenericActivity $activity */</span> 
<span class="token variable">$activity</span> <span class="token operator">=</span> <span class="token class-name static-context">Workflow</span><span class="token operator">::</span><span class="token function">newActivityStub</span><span class="token punctuation">(</span><span class="token variable">$activityClass</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$ids</span> <span class="token operator">=</span> <span class="token keyword">yield</span> <span class="token variable">$activity</span><span class="token operator">-></span><span class="token function">getIds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>(Irrelevant arguments are cut from this example, see the <code>GenericWorkflowBase</code> class shipping with the module for the rest.)</p>
<p>Calling a method on the activity stub returns a promise (from the ReactPHP package) that encapsulates this method invocation. Then <code>yield</code> hands back control to the Temporal PHP SDK, which resolves this promise by sending the activity to the Temporal server as a gRPC request. (Yes, <code>yield</code> can have a value, see the documentation for the rarely used <a href="https://www.php.net/manual/en/generator.send.php" target="_blank" rel="noopener noreferrer">Generator::send()</a> for more.) It is not necessary to yield after every call: see <code>ParallelGenericWorkflow</code> for an example on how to instruct Temporal to run multiple activities in parallel.</p>
<p>Once you are used to this calling convention this is much easier to read than a traditional request builder. Now you can see why the code uses the old <code>/** @var */</code> convention instead of asserting the type directly: as far as the IDE and the developer is concerned, <code>$activity</code> can be treated as an instance of <code>$activityClass</code>. But in reality, it's an <code>ActivityProxy</code> class.</p>
<p>When the Temporal server gets the request to call an activity, it might just send the relevant answer immediately if it is replaying the event log. Otherwise, it logs the activity inputs and puts the request in a task queue. The task queues are processed by workers, we will get back to them after discussing activities. First let's mention the two example workflows shipped with the module. Both call an activity for a large list of IDs (by default 1000), then small chunks of these (by default 20) are sent back to the activity for processing. One workflow launches a chunk worth of activities in parallel, the other sends the chunk in a single call. The former is good for something like search indexing; the latter is good for anything that writes the database and wants to keep database load lower by keeping many writes in a single transaction. A lot of tasks can be accomplished by writing an activity for one of these two, so writing a workflow is not always necessary.</p>
<h2>Coding an Activity</h2>
<p>Writing activities is much easier: the code does not need to talk to Temporal, these are normal Drupal plugins containing ordinary Drupal code and writing Drupal code is very easy ;). As usual for plugins, they need to be within a specific namespace, <code>Drupal\mymodule\Temporal\Activity</code> with the <code>Temporal\Activity\ActivityInterface</code> attribute on the class which, again, is enough for Temporal as well to recognize it as an activity. Methods are marked with the <code>ActivityMethod</code> attribute for Temporal.</p>
<p>While the code doesn't need to contain calls to Temporal, there are some considerations knowing they will be used by Temporal:</p>
<ul>
<li>As activity calls are remote calls in disguise, both the arguments and the return values need to be serializable.</li>
<li>If there is an error – typically in calling third party services – that warrants a retry then the activity can simply let the exception propagate. The SDK will catch it and pass it to Temporal. For more fine-grained control the activity can throw an <code>ApplicationFailure</code>, which, among others, can tell Temporal whether the failure can be retried at all.</li>
<li>Heartbeats are super easy to send: <code>Activity::heartbeat($progress);</code>
It really must be noted how easy it is to work with the Temporal PHP SDK. We have a complex server-client architecture but most of the complexity is not visible at all: activity calls are hidden behind a proxy class and a simple yield, progress reporting is a single static method call and simple error handling is completely automatic.</li>
</ul>
<p>An example activity is shipped with the module which re-saves every entity of an entity type.</p>
<h2>Actually Trying It</h2>
<p>Before we can get to trying it, there's one more thing we need to introduce: Workers. These are long-running processes that poll the Temporal task queues and run workflows and activities. To better support their long-running nature, Temporal uses the <a href="https://roadrunner.dev/" target="_blank" rel="noopener noreferrer">RoadRunner</a> application server for them. The Drupal integration ships this worker as a Drush command and supplies a sample <code>.rr.yaml</code> RoadRunner configuration to run this command. Most of the time simply copying the configuration to the project root and running <code>rr serve</code> is all you need to do. Read the module <code>README.md</code> for more. Besides a Temporal server instance at least one worker is needed for Temporal to work. But you can run as many as the workload warrants.</p>
<p>To round it off, there's a Drush command to start workflows and another to send signals and queries to them.</p>
<p>To make local development easier, a DDEV add-on (<a href="https://addons.ddev.com/addons/tag1consulting/ddev-temporalio" target="_blank" rel="noopener noreferrer">tag1consulting/ddev-temporalio</a>) has been developed as well, this spins up a Temporal server and a Temporal web UI. So in a ddev project, you can run</p>
<pre><code>ddev get tag1consulting/ddev-temporalio
ddev restart
ddev composer require 'drupal/temporal:^2.1'
ddev drush en -y temporal
ddev drush temporal:workflow:start 'Drupal\temporal\Temporal\Workflow\GenericWorkflow' 'Drupal\temporal\Temporal\Activity\EntityResave' user
</code></pre>
<p>This will re-save every user entity. The first argument of the Drush <code>temporal:workflow:start</code> command is the name of the workflow. In turn, the first argument of this particular workflow is the activity class. This is not a Temporal convention or even a convention of the Drupal-temporal integration, it's simply convenient for such a generic workflow. The rest of the arguments are passed to the activity and the entity resave activity needs the entity type.</p>
<p>We started with talking about batch, let's finish with it, too: we actually integrated the Drupal batch system with temporal. In the next blog post we will talk about that. Teaser: you can start the batch and close the browser tab.</p>
<hr />
<p>Running into the limits of Drupal's batch and queue? <a href="https://www.tag1.com/drupal/">See how Tag1 scales Drupal.</a></p>
]]></description>
      <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Károly Négyesi</dc:creator>
      <guid>https://www.tag1.com/blog/temporal-integration-with-drupal/</guid>
    </item>
    
    
    <item>
      <title>Building a Production-Ready Drupal Module in a Weekend with AI: The LinkStash Story</title>
      <link>https://www.tag1.com/blog/production-ready-drupal-module-in-a-weekend/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
        <p>At Tag1, we believe in proving AI within our own work before recommending it to clients. This post is part of our <a href="https://www.tag1.com/insights/?tag=AI#selected">AI Applied</a> content series, where team members share real stories of how they're using Artificial Intelligence and the insights and lessons they learn along the way. Here, <a href="https://www.tag1.com/team/#d%C3%A9nes-szab%C3%B3">Dénes Szabó</a> (Drupal Developer) built <a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">LinkStash</a>, a production-ready Drupal 11 bookmarking module, in one weekend using Claude Sonnet 4.5.</p>
    </div>
</div>
</div>
<h2>The Browser Tab Apocalypse</h2>
<p>You know the feeling. It's Tuesday afternoon, you have 47 browser tabs open across three different browsers, and somewhere in that digital haystack is that one article you absolutely need to reference. Chrome has the documentation you bookmarked last week. Firefox has the GitHub issues you were reviewing. Safari has... honestly, you can't remember what Safari has anymore. Your browser's &quot;Reading List&quot; feature is laughing at you. Your bookmarks folder looks like a digital hoarder's attic. And don't even get started on those &quot;bookmark this page to read later&quot; services that require uploading all your data to someone else's server.</p>
<p>As a Drupal developer, I looked at this chaos and thought: &quot;I could fix this. I have the technology. I have the skills. I have... absolutely no time to actually build it because I'm too busy managing 47 browser tabs.&quot;</p>
<p>So naturally, I decided to see if AI could help me build it in a weekend instead.</p>
<h2>The Experiment: Can AI Build a Real Drupal Module?</h2>
<p>The goal was ambitious but clear: build <a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">LinkStash</a>, a personal bookmarking tool, as a production-ready Drupal 11 contrib module suitable for release on <a href="https://drupal.org/" target="_blank" rel="noopener noreferrer">drupal.org</a>. Not a prototype. Not a proof-of-concept. A real, tested, documented, standards-compliant module that follows all of Drupal's best practices and passes the strict quality requirements for the official repository.</p>
<p>The feature list was substantial: entity system with full CRUD operations, browser bookmarklet for one-click saving, automatic metadata fetching with SSRF protection, smart domain-based auto-categorization, video embed support for YouTube and Facebook, Views integration with filters, 100% PHPCS compliance, PHPStan Level 1 static analysis, and a full test suite. Oh, and proper documentation including README, CHANGELOG, and API docs.</p>
<p>You know, just a light weekend project. What could possibly go wrong?</p>
<h2>Why Claude Sonnet 4.5, Not Opus?</h2>
<p>Here's where it gets interesting. Everyone assumes you need the biggest, most powerful AI model for serious development work. But I deliberately chose Claude Sonnet 4.5 instead of Opus, and here's why: it's way cheaper, significantly faster, and (here's the kicker) equally clever for code generation.</p>
<p>For structured development tasks with clear requirements, Sonnet 4.5 absolutely shines. It understands Drupal architecture, follows coding standards precisely, writes comprehensive tests, and generates production-quality code. The speed difference is noticeable: responses come back in seconds instead of tens of seconds. When you're iterating on test failures or fixing PHPCS violations, that speed compounds into serious time savings.</p>
<p>The cost difference? Even more dramatic. We're talking roughly one-fifth the cost per token. Over the course of building <a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">LinkStash</a>, which consumed an estimated 200-250K tokens across multiple development sessions, Sonnet 4.5 probably cost around $3-$5 total.</p>
<div class="callout-box">
<p>That's less than a fancy coffee, for a complete, production-ready Drupal module. Let that sink in.</p>
</div>
<h2>The Human-AI Collaboration: How We Actually Built It</h2>
<p>AI didn't build this module alone. This was a collaboration, and understanding the dynamics matters.</p>
<p><strong>What AI did (the heavy lifting):</strong></p>
<ul>
<li>Generated all entity boilerplate following Drupal 11 patterns</li>
<li>Implemented three complete plugin systems (ContentFetcher, CategoryRule, MediaProvider)</li>
<li>Wrote 187 tests (132 unit + 55 kernel) with proper fixtures and mocks</li>
<li>Wrote the documentation (README, CHANGELOG, API docs)</li>
<li>Fixed all PHPCS and PHPStan violations autonomously</li>
<li>Debugged test failures systematically (13 failures across 3 test suites, all resolved)</li>
<li>Generated field configurations, Views configs, and taxonomy vocabularies</li>
</ul>
<p><strong>What I did (the professional supervision):</strong></p>
<ul>
<li>Created the Product Requirements Document with the 3-phase roadmap</li>
<li>Made all architectural decisions (entity structure, plugin patterns, security model)</li>
<li>Decided on module naming (checked <a href="http://drupal.org/">drupal.org</a> availability, chose <a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">&quot;LinkStash&quot;</a>)</li>
<li>Identified critical security requirements (SSRF protection, XSS prevention)</li>
<li>Reviewed and corrected AI assumptions when they didn't match Drupal realities</li>
<li>Ran the actual tests in DDEV (AI can't access Docker environments)</li>
<li>Caught edge cases AI missed (like the DNS rebinding vulnerability in SSRF protection)</li>
<li>Made final calls on trade-offs (documented limitations vs. complex solutions)</li>
</ul>
<h2>When I Had to Intervene: The Learning Moments</h2>
<p>The most surprising part wasn't what went smoothly; it was where AI stumbled and needed human expertise.</p>
<p><strong>The constant mystery.</strong> AI kept trying to use <code>EntityStorageInterface::SAVED_NEW</code>, but that constant doesn't live in that interface. It's a global constant in <code>core/includes/common.inc</code>. I had to explicitly correct this: &quot;This constant does not exist in this interface. It's defined in common.inc. Fix it.&quot; The AI course-corrected immediately.</p>
<p><strong>The access control saga.</strong> All seven access tests were failing despite correct permissions and entity ownership. AI tried using <code>AccessResult::allowedIf()</code>, which should work, but didn't in Drupal 11's kernel test environment. After systematic debugging, I suggested trying explicit conditionals instead. That fixed it instantly; it was a behavioral quirk AI wouldn't have discovered alone.</p>
<p><strong>The vocabulary ID confusion.</strong> Tests were creating a <code>tags</code> vocabulary, but the actual config created <code>linkstash_tags</code>. AI confidently wrote tests that failed due to this mismatch. Human pattern recognition caught it: &quot;You're testing against the wrong vocabulary ID.&quot;</p>
<p><strong>The YouTube ID format.</strong> AI used 3-character test IDs (<code>abc</code>, <code>xyz</code>), but YouTube video IDs are exactly 11 characters using base64url encoding. The regex pattern <code>[a-zA-Z0-9_-]{11}</code> doesn't lie. Once I pointed out that YouTube IDs are standardized at 11 characters, AI immediately generated valid test data.</p>
<div class="callout-box">
<p>These weren't AI failures; they were collaboration points. AI had the speed to generate code and tests. I had the domain expertise to catch subtle Drupal-specific issues. Together, we debugged faster than either of us could alone.</p>
</div>
<h2>The Development Speed</h2>
<p>The numbers are worth a look:</p>
<ul>
<li><strong>Calendar time:</strong> Friday/Saturday to Sunday (February 7-9, 2026)</li>
<li><strong>My active time:</strong> 10-20 hours of supervision, decision-making, and testing</li>
<li><strong>AI sessions:</strong> Multiple sessions totaling ~200-250K tokens</li>
<li><strong>Code generated:</strong> 8,000+ lines across entity classes, plugins, services, tests, and configs</li>
<li><strong>Test suite:</strong> 187 tests written and debugged to 100% pass rate</li>
<li><strong>Documentation:</strong> 1,800+ lines (README, CHANGELOG, module intro HTML)</li>
</ul>
<p>Here's what &quot;one weekend&quot; actually delivered:</p>
<ul>
<li>Complete entity system with 12 fields</li>
<li>Three working plugin systems with 8 plugin implementations</li>
<li>Browser bookmarklet with popup fallback logic</li>
<li>Automatic metadata fetching with SSRF protection</li>
<li>Smart auto-categorization based on 5 domain-matching rules</li>
<li>YouTube and oEmbed video embed support</li>
<li>Two Views (list + detail) with exposed filters</li>
<li>187 tests (100% passing, ~85% code coverage)</li>
<li>Zero PHPCS violations, PHPStan Level 1 clean</li>
<li>Complete documentation ready for <a href="http://drupal.org/">drupal.org</a> submission</li>
</ul>
<p>The security model deserves a callout: SSRF protection blocks private IP ranges, local thumbnail storage prevents IP leaking, and all data is isolated per user. All three plugin systems are also fully extensible, other developers can add custom content fetchers, categorization rules, and media providers. The module ships with sensible defaults but is built for extension.</p>
<p>Could I have built this solo in a weekend without AI? Absolutely not. The test suite alone would have taken days. Could AI have built this without supervision? Also no. Those subtle Drupal-specific issues required experienced judgment calls.</p>
<p>But together? We shipped a release-ready beta in 48 hours.</p>
<h2>The Numbers: What Did This Actually Cost?</h2>
<p>Let's break down the economics.</p>
<p><strong>Time investment:</strong></p>
<ul>
<li>Human time: ~15 hours (let's split the 10-20 range)</li>
<li>AI compute time: Maybe 30-45 minutes total across all sessions</li>
<li>Calendar time: One weekend</li>
</ul>
<p><strong>Token usage:</strong></p>
<ul>
<li>Estimated total: 200-250K tokens (spanning multiple sessions)</li>
<li>Breakdown by task (from <code>progress.md</code>):
<ul>
<li>Project setup: ~10K tokens</li>
<li>Entity/field architecture: ~14K tokens</li>
<li>Major features (8-10): ~15K tokens each</li>
<li>Test suite (187 tests): ~30K tokens</li>
<li>Code review + fixes: ~15K tokens</li>
<li>Documentation: ~20K tokens</li>
</ul>
</li>
</ul>
<p><strong>Actual costs (Claude Sonnet 4.5 pricing):</strong></p>
<ul>
<li>Input tokens: ~$3 per million tokens</li>
<li>Output tokens: ~$15 per million tokens</li>
<li>Rough estimate: $3-$5 total for the entire project</li>
</ul>
<p><strong>Traditional development cost (rough estimate):</strong></p>
<ul>
<li>Senior Drupal developer: $100-$150/hour</li>
<li>Time required (solo): 40-60 hours (conservative)</li>
<li>Traditional cost: $4,000-$9,000</li>
</ul>
<p>The AI-assisted approach wasn't just faster; it was two to three orders of magnitude cheaper while maintaining professional quality standards.</p>
<h2>The Future: Where Do We Go From Here?</h2>
<p>Building v1 in a weekend was exhilarating, but it's just the beginning. The roadmap has two more phases.</p>
<p><strong>Phase v2 (enhanced features):</strong></p>
<ul>
<li>Browser extension for Chrome/Firefox (deeper integration than a bookmarklet)</li>
<li>Import/export (JSON, CSV, HTML bookmark files)</li>
<li>Link health checking (detect broken links via cron)</li>
<li>Duplicate detection and merging</li>
<li>Collections system (folders, boards, groups)</li>
<li>Full-text content archival (Wayback Machine style)</li>
</ul>
<p><strong>Phase v3 (advanced capabilities):</strong></p>
<ul>
<li>AI-powered auto-tagging using actual page content (not just domain matching)</li>
<li>Smart recommendations based on browsing patterns</li>
<li>Multi-site synchronization for users with multiple Drupal sites</li>
<li>Drupal Recipe for one-click installation</li>
<li>Social features (public collections, sharing)</li>
</ul>
<p>The best part? Now that the architecture is solid and the plugin systems are proven, adding these features becomes incremental. Each new feature is just another plugin implementation or service extension. The hard architectural work is done.</p>
<h2>Was It Fun? (Spoiler: Yes, Incredibly)</h2>
<p>Nobody warns you that AI-assisted development is fun.</p>
<p>There's something magical about describing what you want, for example, &quot;Now we need a plugin system for content fetchers that can pull metadata from any URL with SSRF protection,&quot; and watching structured, working code appear in seconds. Then catching a subtle bug, pointing it out, and watching the immediate course correction. It feels less like programming and more like conducting. You're directing the architecture and reviewing the output, not typing every curly brace.</p>
<p>The debugging sessions were particularly entertaining. When all seven access tests failed, AI and I became detective partners:</p>
<blockquote>
<p><strong>Me:</strong> &quot;Add debug assertions before the access check. Let's see if the user actually has permissions.&quot;</p>
<p><strong>AI:</strong> Adds assertions. &quot;Okay, permissions confirmed. User owns the entity too.&quot;</p>
<p><strong>Me:</strong> &quot;So why is <code>AccessResult::allowedIf()</code> not working?&quot;</p>
<p><strong>AI:</strong> &quot;Let me try explicit conditionals instead...&quot;</p>
<p><strong>Me:</strong> &quot;That... actually fixed all seven tests. Interesting.&quot;</p>
</blockquote>
<p>That back-and-forth, that collaborative debugging energy, reminded me of pair programming with a really fast typist who never gets tired but occasionally needs you to remember obscure Drupal API behaviors.</p>
<p>The satisfaction of running <code>ddev composer run-tests</code> and seeing &quot;187 tests, 846 assertions, OK&quot; flash green? That hit the same way shipping your first module to production does. Except this time, we'd done it in a weekend instead of a month.</p>
<h2>The Real Lesson: Augmentation, Not Replacement</h2>
<p>If there's one takeaway from this experiment, it's this: AI is an amplifier, not a replacement.</p>
<p>I couldn't have built <a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">LinkStash</a> in a weekend alone. But AI couldn't have built it without me either, not to production quality, not with proper security considerations, not with the architectural decisions that make it extensible and maintainable.</p>
<p>What AI gave me was force multiplication. Instead of typing boilerplate entity code, I specified requirements and reviewed output. Instead of writing 187 tests manually, I described what needed testing and verified the results. Instead of formatting documentation, I outlined structure and AI filled in details.</p>
<div class="callout-box">
<p>I stayed in the driver's seat for architecture, security, and critical decisions. AI handled the mechanical work of code generation, test writing, and standards compliance. Together, we shipped something neither of us could have done alone in the same timeframe.</p>
</div>
<p>And honestly? For a problem that's been in the back of my mind for months, &quot;I really need a better bookmarking solution,&quot; going from idea to a release-ready beta in one weekend feels like the future.</p>
<p>Now if you'll excuse me, I need to actually use <a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">LinkStash</a> to bookmark all the tabs I've accumulated while writing this blog post. The irony is not lost on me.</p>
<p><a href="https://www.drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">LinkStash</a> is available at <a href="https://drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">drupal.org/project/linkstash</a>. Built with Claude Sonnet 4.5 via Claude Code. Source code, documentation, and contribution guidelines are available in the project repository.</p>
<p><strong>Development metrics:</strong> 1 weekend, ~15 human hours, ~240K AI tokens, $3-5 compute cost, 8,000+ lines of code, 187 tests, 0 PHPCS violations, <a href="https://drupal.org/project/linkstash" target="_blank" rel="noopener noreferrer">live on drupal.org</a>.</p>
<p>If you're weighing whether AI can carry real weight on your next Drupal contrib module, or you want to talk through where human oversight still has to stay in the loop, let's start a conversation! <a href="https://www.tag1.com/contact/">We'd love to hear from you.</a></p>
<div class="highlight-box">
<p>This post is part of Tag1’s <a href="https://www.tag1.com/insights/?tag=AI&type=blogPosts#selected">AI Applied content series</a>, where we share how we're using AI inside our own work before bringing it to clients. Our goal is to be transparent about what works, what doesn’t, and what we're still figuring out, so that together, we can build a more practical, responsible path for AI adoption.</p>
</div>
<p>Image by <a href="https://www.pexels.com/@mahmoudramadan/" target="_blank" rel="noopener noreferrer">Mahmoud Ramadan</a> from <a href="https://www.pexels.com/photo/abstract-wave-form-with-gradient-lighting-31622977/" target="_blank" rel="noopener noreferrer">pexels</a></p>
]]></description>
      <pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Dénes Szabó</dc:creator>
      <guid>https://www.tag1.com/blog/production-ready-drupal-module-in-a-weekend/</guid>
    </item>
    
    
    <item>
      <title>Scolta on Drupal</title>
      <link>https://www.tag1.com/how-to/how-to-scolta-on-drupal/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<p>Imagine a user visits an encyclopedia website and searches for &quot;survival in extreme conditions.&quot; The article they want, about the Ross Sea party being stranded in Antarctica for two years, doesn't contain that phrase anywhere. Nor do dozens of others that would match what they're hoping to find. Standard keyword search can't connect the query to any of them, because the visitor's specific words don't appear in any of the pages they're looking for.</p>
<p>You can try to close that gap with synonym lists, stemming, or manually tagging content with the terms you think people will use. But you can't control what people type, and they're going to use their own words. <strong>That gap between what someone types and what your content actually says is the problem Scolta solves.</strong></p>
<p>Start with our introduction to Scolta and the thinking behind it in <a href="https://www.tag1.com/blog/introducing-scolta/">Introducing Scolta</a>: what it is, why there's no vector database or embedding pipeline in the picture, and how the four-stage search pipeline fits together (the architecture itself goes deeper in <a href="https://www.tag1.com/how-to/the-practical-path-to-ai-search/">The Practical Path to AI Search</a>). This how to guide is the hands-on companion for Drupal: install the module, point it at your content, and tune it until that example query works. Everything here is open source as a <a href="https://www.drupal.org/project/scolta" target="_blank"> Drupal module</a>. Later posts in this series do the same for the other platforms, WordPress next.</p>
<h2>The Athenaeum: Over 6,000 Wikipedia Articles in Drupal</h2>
<p>To properly demonstrate Scolta on Drupal, we needed a demo corpus that was large, familiar, and verifiable. We couldn't do meaningful testing with <a href="https://en.wikipedia.org/wiki/Lorem_ipsum" target="_blank">lorem ipsum</a>, and a handful of manually crafted test pages wasn't enough. We needed something where a reader could look at the search results and quickly understand whether they made sense.</p>
<p>Wikipedia's <a href="https://en.wikipedia.org/wiki/Wikipedia:Featured_articles" target="_blank"> Featured Articles</a> turned out to be perfect. These are the most rigorously reviewed entries in the English Wikipedia. We pulled over 6,000 of them spanning every domain of human knowledge: science, history, biography, geography, arts, technology, nature, military history, sports, philosophy. Each article averages 4,000 to 8,000 words with their full section structure preserved.</p>
<p>We built the demo site on <a href="https://scolta-demo-drupal-pedia.tag1.ai/" target="_blank">Drupal 11 and called it The Athenaeum</a> (it's themed with a library reading room aesthetic: parchment backgrounds, navy headings, burgundy accents, and a library card catalog motif in the search UI). All content is CC BY-SA 4.0 with attribution links back to the original Wikipedia articles on every page. So the <a href="https://scolta-demo-drupal-pedia.tag1.ai/" target="_blank">demo</a> uses real content, real licensing, and nothing proprietary.</p>
<p>This breadth is what makes the demo work: cross-domain discovery actually happens. A site about one topic can fake good search results with just keywords. An encyclopedia covering everything from quantum mechanics to the Battle of Gettysburg to bowerbird mating rituals can quickly fall apart with standard keyword search.</p>
<h2>Installing Scolta on Drupal</h2>
<p>Scolta integrates with Drupal through the <a href="https://www.drupal.org/project/search_api" target="_blank"> Search API</a> module, which is Drupal's established abstraction layer for search backends. If you've ever configured Solr or Elasticsearch for a Drupal site before, the workflow is familiar, but without infrastructure overhead. If not, it's still straightforward. Scolta is just another Search API backend, which means existing views, facets, and search pages keep working.</p>
<h3>Step 1: Install Scolta with Composer the Way You'd Install Any Contributed Module:</h3>
<pre><code>composer require drupal/scolta
drush en scolta
</code></pre>
<p>Composer pulls in tag1/scolta-php (a shared library that makes Scolta work across Drupal, WordPress, Laravel, and custom PHP applications) and drupal/search_api automatically. The module works on Drupal 10.3+ and 11.x with PHP 8.1+.</p>
<h3>Step 2: Set up Search API</h3>
<ol>
<li>Go to Administration &gt; Configuration &gt; Search and metadata &gt; Search API</li>
<li>Add a server, select &quot;Scolta (Pagefind)&quot; as the backend</li>
<li>Create an index pointing to that server</li>
<li>Choose which content types to index: in our case, the Featured Article content type with title, body, and taxonomy fields</li>
</ol>
<h3>Step 3: Build the Index</h3>
<pre><code>drush scolta:build --force
</code></pre>
<p>This does two things. First, Scolta exports your Drupal content as static HTML, one file per indexed node, with the title, body, taxonomy, and any other fields you configured in the Search API index. Second, it builds the search index from that exported content using <a href="https://pagefind.app/" target="_blank"> Pagefind</a>, the static search library that scolta-core (the Rust/WASM engine from Introducing Scolta) builds on. The index runs entirely in the visitor's browser, which is why there's no Solr, Elasticsearch, or search daemon anywhere in these instructions.</p>
<p>For over 6,000 articles averaging thousands of words each, the build takes a few minutes on a decent server. The default PHP indexer needs no binary, no Node.js, and no exec(), so it runs even on shared hosting with a 128M memory limit.</p>
<h3>Step 4: Connect an AI Provider</h3>
<p>Scolta gives you three ways to wire this up:</p>
<ol>
<li><strong>Zero-config (<a href="http://amazee.ai/" target="_blank">Amazee.ai</a> free trial):</strong> out of the box you don't have to configure anything. On the first AI request, Scolta auto-provisions a free trial provided by <a href="http://amazee.ai/" target="_blank">Amazee.ai</a>, a hosted LLM gateway that works immediately. When the generous trial ends, you'll be prompted to enter an email address if you wish to upgrade to a paid account.</li>
<li><strong>Direct provider connection:</strong> alternatively you can configure your own Anthropic or OpenAI API key in an environment variable (SCOLTA_API_KEY=sk-ant-...) or in settings.php. Scolta talks directly to the provider's API. You control the model, the costs, and the data flow without any additional dependencies.</li>
<li><strong>Drupal AI Initiative module:</strong> If you're already using the <a href="https://www.drupal.org/project/ai" target="_blank"> Drupal AI</a> modules, Drupal's community-built abstraction layer for AI providers, Scolta integrates with it directly. Select &quot;Drupal AI module&quot; as the provider in Scolta's admin settings, and Scolta delegates all AI calls to whatever provider you've configured through the Drupal AI module. That means any provider the module supports (Anthropic, OpenAI, <a href="http://amazee.ai/">Amazee.ai</a>, Ollama, AWS Bedrock, Azure OpenAI, and a growing list of others) works with Scolta automatically. You manage one provider configuration for your entire site instead of configuring each module separately. This is the recommended setup for Drupal sites that already use the AI module for other features like content generation or translation.</li>
</ol>
<p>The admin form at /admin/config/search/scolta shows all available options in a dropdown. When you select the Drupal AI provider, the API key and model fields hide themselves, as at that point those settings come from the Drupal AI module's own configuration.</p>
<h3>Step 5: Pick a Preset and You're Done</h3>
<p>The next most important setting on the Scolta admin page is the &quot;Site Type&quot; dropdown asking what kind of site this is. Five options get you started:</p>
<ul>
<li><strong>Start from Scratch</strong>: general purpose defaults</li>
<li><strong>Recipe &amp; Content Catalog</strong>: for structured, timeless, browse-oriented content</li>
<li><strong>Documentation &amp; Reference</strong>: for knowledge bases and domain-specific references</li>
<li><strong>E-commerce &amp; Product Store</strong>: for product catalogs</li>
<li><strong>Blog &amp; Editorial</strong>: for narrative and editorial content</li>
</ul>
<p>For a Wikipedia-style encyclopedia, &quot;Recipe &amp; Content Catalog&quot; is the right choice. The name may suggest cooking sites, but the preset is really for structured, timeless, browse-oriented collections, which includes an encyclopedia. There's also a &quot;Documentation &amp; Reference&quot; preset, but it's tuned more for vocabulary bridging (patients searching &quot;my head hurts&quot; on a medical site where the content is more technical), which a later post in this series covers. Wikipedia readers generally search with the right terminology already, so the catalog preset fits better. Select &quot;Recipe &amp; Content Catalog&quot;, save, and the scoring parameters update to sensible defaults for encyclopedic content.</p>
<p>At this point you have a working AI search. Drop the Scolta Search block onto a page via Block Layout, and your users can start searching.</p>
<p>That's the &quot;just works&quot; path. It's maybe five minutes if you already know Drupal's Search API workflow. It's a fantastic starting place, but there's a lot of tuning possible.</p>
<h3>What the Preset Actually Does (and When to Go Further)</h3>
<p>The <code>content_catalog</code> preset makes three scoring changes that matter for an encyclopedia:</p>
<ol>
<li><strong>Recency is disabled.</strong> The default recency strategy uses exponential decay, meaning newer content gets a ranking boost and older content gradually sinks in the search results. For a news site, that makes sense. For an encyclopedia, it's exactly wrong. An article about Roman aqueducts is as relevant today as the day it was indexed. The site type preset we've selected sets recencyStrategy to none.</li>
<li><strong>Full-title matches get a bigger reward.</strong> The default title boost already favors titles, and the preset leaves it alone. What it raises is <code>title_all_terms_multiplier</code>, from 1.5 to 2.5: when every word of the query appears in the title, that article gets a strong extra push. Encyclopedia titles are precise identifiers, &quot;Battle of Gettysburg,&quot; &quot;Quantum mechanics,&quot; &quot;Cleopatra,&quot; and when someone types one, the article carrying that title should rank first. The multiplier makes that happen without drowning out body-text matches for broader queries.</li>
<li><strong>Body text gets a bump.</strong> content_match_boost goes from 0.4 to 0.5. While a small change, it does impact search performance. Encyclopedia articles have rich body text where the cross-domain connections live. The article about bowerbirds doesn't have &quot;architecture&quot; in its title, but the body text describes elaborate structures the birds build. Raising the content boost from 0.4 to 0.5 makes sure those body-text connections are found by relevant search queries.</li>
</ol>
<p>The preset also widens the funnel: Pagefind fetches 75 candidate results instead of 50 before re-ranking, the AI summary draws from the top 15 results instead of 10, and result pages show 12 instead of 10, because browse-oriented sites reward breadth. Each preset is a starting point, not a cage; every individual parameter can be overridden in the Scoring section of the admin form (collapsed by default, because most people don't need it).</p>
<h2>The Real Quality Lever: Site Description</h2>
<p>Because Scolta is using Large Language Models (LLMs), the single most important configuration field isn't in the scoring section at all. It's the site description.</p>
<p>The site description is a plain text field in Scolta's Content section. Whatever you put there gets passed directly to the AI model at query expansion time. It's the context that tells whatever model you're using what kind of content it's working with.</p>
<p>For The Athenaeum, the description says the content spans science, history, biography, geography, arts, and technology, and that it's an encyclopedia covering all areas of human knowledge. That description does more for search quality than any scoring parameter adjustment.</p>
<p>When someone searches for a concept, say, &quot;survival in extreme conditions&quot;, the AI expands that into specific search terms. Without a good site description, the expansion might focus narrowly, maybe just on survival gear or wilderness tips. With a description that says &quot;cross-domain encyclopedia,&quot; the AI knows to think broadly. It expands to Antarctic exploration, extremophile bacteria, deep-sea life, space missions, mountain climbing. The expansion matches the content because the description matches the content.</p>
<div class="callout-box">
<p>A great site description with default scoring parameters will outperform a generic description with perfectly tuned scoring. It's held true on every site we've built with Scolta. Tune the description first. Tune the numbers second.</p>
</div>
<p>We started with scoring parameters because that's what search engineers expect to tune. It's what we'd reach for on Solr or Elasticsearch. But AI search inverts the priority: the context you give the model matters more than the weights you assign to fields. If you only have five minutes, spend them on the site description.</p>
<h2>Show It Working</h2>
<p>Here are two queries that show what Scolta does that keyword search can't. Because Scolta uses an LLM to expand each query, the exact results and AI Overview shift from search to search: the articles in the index don't change, but the expanded terms do, so you see a different slice of the same corpus each time. These reflect what we saw writing this post.</p>
<ul>
<li>&quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=tiny+things+with+huge+impact" target="_blank">tiny things with huge impact</a>&quot; comes back with DNA nanotechnology, Niels Bohr (the subatomic scale determining all of chemistry), the periodic table, and Gothic boxwood miniatures, medieval carvings a few centimeters across whose &quot;spiritual impact [was] curiously in inverse proportion to their size.&quot; It even pulls in Pluto's reclassification, a tiny world that reshaped what &quot;planet&quot; means. None of these pages say &quot;tiny things&quot;; the expansion found smallness expressed as nanotechnology, atomic physics, and miniature art.</li>
<li>&quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=beautiful+mathematics" target="_blank">beautiful mathematics</a>&quot; is the clearer case, since neither word alone would surface what comes back. The Aesthetics article leads, quoted directly on when &quot;a mathematical proof may be considered beautiful.&quot; Group theory comes back as the study of symmetry, and Palladian architecture arrives through Colin Rowe's &quot;The Mathematics of the Ideal Villa,&quot; connecting mathematical elegance to physical form. &quot;Beautiful mathematics&quot; isn't a keyword phrase, it's a concept living where aesthetics and formal reasoning meet, and the query expansion finds articles at that intersection.</li>
</ul>
<p>Run the same two queries on any keyword search engine and compare.</p>
<h2>The AI Overview Sees Your Data, Not Just Your Text</h2>
<p>Finding the right articles is half the problem. The other half is what the AI does with them when it writes a summary.</p>
<p>Most AI search implementations feed the LLM a title, a URL, and a text excerpt per result. That's enough to generate a paragraph that sounds reasonable. But &quot;sounds reasonable&quot; and &quot;actually correct&quot; aren't always the same thing. Ask &quot;which articles have the most citations&quot; and the AI can only guess since it sees the article text but not the citation count. Ask &quot;first article published&quot; and it may hallucinate a date if it couldn't see the actual publish dates.</p>
<div class="callout-box">
<p>Scolta solves this by passing all indexed metadata to the AI alongside each result. Not just the text, but also every structured field in the index: word count, reference count, date, taxonomy, and whatever you've configured as sortable or filterable.</p>
</div>
<p>Each field is labeled so the AI knows what it's looking at, and if the user sorted or filtered their results, the AI sees that too. In this way it knows which field was sorted, in which direction, and which filters were applied, and it uses this knowledge when responding to site visitors.</p>
<p>For The Athenaeum, that means the AI overview can reference actual numbers. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=the+longest+articles+about+science" target="_blank">The longest articles about science</a>&quot; doesn't produce a vague summary about science topics, it produces a summary that cites specific word counts because the AI can see the <code>word_count</code> field for each result. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=most+cited+articles+about+history" target="_blank">Most cited articles about history</a>&quot; references real citation counts. Scolta guides the AI response with real data, minimizing hallucinations.</p>
<h2>When Users Want Results in a Specific Order</h2>
<p>So far we've been talking about finding the right articles, and about the AI summarizing them accurately. Sometimes users also want those results in a particular order: &quot;longest articles about science,&quot; &quot;most cited articles about history,&quot; &quot;newest articles about chemistry.&quot; Each of those has a sort intent baked into the query, and because Scolta already passes the AI your structured fields, it can act on it.</p>
<p>Scolta detects this automatically. When someone types &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=longest+articles+about+science" target="_blank">longest articles about science</a>,&quot; Scolta's AI expansion pipeline recognizes two things: the user wants articles about science (the search part), and they want those articles sorted by length (the sort part). The search results come back re-ranked by word count, highest first, with a sort badge below the search box showing &quot;Sorted by: word_count (highest first)&quot; and a dismiss button if you'd rather go back to purely relevance ordering.</p>
<p>&quot;Longest articles about science&quot; returns science-related articles sorted by their actual word count, with the AI overview citing specific word counts because it can see the metadata. On a recent run, the top results included Periodic table (29,840 words), J. Robert Oppenheimer (20,025 words), Plutonium (17,024 words), and Otto Hahn (16,291 words). Your results will differ because Scolta uses an LLM to expand the query, and different expansions surface different articles. The word counts are real and come from the indexed metadata, but which articles the expansion selects as &quot;science&quot; will vary from search to search.</p>
<p>&quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=most+cited+articles" target="_blank">Most cited articles</a>&quot; works the same way, sorting by reference count. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=newest+articles+about+chemistry" target="_blank">Newest articles about chemistry</a>&quot; sorts by date. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=shortest+science+articles" target="_blank">Shortest science articles</a>&quot; sorts ascending. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=articles+about+wars+sorted+by+date" target="_blank">Articles about wars sorted by date</a>&quot; demonstrates explicit sort syntax, the user literally says &quot;sorted by&quot; and Scolta catches it regardless of how complex the rest of the query is.</p>
<p>Scolta also knows when not to sort. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=best+practices+for+scientific+writing" target="_blank">Best practices for scientific writing</a>&quot; returns matching results in relevance order, with no sort badge. &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/search?q=most+common+elements" target="_blank">Most common elements</a>&quot; is a discovery query, the user is looking for well-known elements, not trying to sort by some metric. Of course, classification isn't perfect and edge cases exist where it may not work as you intend, especially when the same word could mean &quot;sort by this metric&quot; or &quot;tell me about this concept&quot; depending on context.</p>
<h3>Configuring Sortable Fields</h3>
<p>Sort detection only works if you tell Scolta which fields are sortable. The admin UI at /admin/config/search/scolta has a Sortable Fields section where you add fields and descriptions through a form.</p>
<p>If you prefer config-as-code, the same thing in YAML:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token comment"># config/sync/scolta.settings.yml</span>
<span class="token key atrule">sortable_fields</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> word_count
  <span class="token punctuation">-</span> date
  <span class="token punctuation">-</span> reference_count
<span class="token key atrule">sortable_field_descriptions</span><span class="token punctuation">:</span>
  <span class="token key atrule">word_count</span><span class="token punctuation">:</span> <span class="token string">'Number of words in the article'</span>
  <span class="token key atrule">date</span><span class="token punctuation">:</span> <span class="token string">'Publication or last-updated date'</span>
  <span class="token key atrule">reference_count</span><span class="token punctuation">:</span> <span class="token string">'Number of references cited in the article'</span></code></pre>
<p>Import the config and clear cache:</p>
<pre><code>drush config:import -y
drush cr
</code></pre>
<p>Note that the field descriptions actually matter and are configuration. They're passed to the AI so it can map user language to field names. When someone types &quot;most cited articles,&quot; the AI reads the description &quot;Number of references cited in the article&quot; and connects &quot;cited&quot; to <code>reference_count</code>. Without the description, it has to guess from the field name alone, and <code>reference_count</code> is less obvious than <code>citation_count</code> would be.</p>
<h3>Adding Your Own Sortable Fields</h3>
<p>The fields available for sorting are the same fields you index in Pagefind. If your content has a structured field, such as a price, a rating, a difficulty level, or a page count, you can make it sortable.</p>
<p>For Drupal, add the field to your Search API index configuration so it gets exported during the Scolta build, then add it to the Sortable Fields section in the Scolta admin page with a plain-language description so the AI can map user intent to the field. (Or add it to <code>sortable_fields in scolta.settings.yml</code> with a corresponding entry in <code>sortable_field_descriptions</code>, as shown in the example above.)</p>
<p>As a concrete example, say your Drupal site has a &quot;Reading Level&quot; field with values 'Beginner', 'Intermediate', and 'Advanced' that you've mapped to numeric values '1', '2', '3' in your content type. Add <code>reading_level</code> to your sortable fields with the description &quot;Reading difficulty level: 1=Beginner, 2=Intermediate, 3=Advanced.&quot; Now &quot;easiest articles about science&quot; returns science articles sorted by reading level ascending. The AI reads the field description, maps &quot;easiest&quot; to &quot;lowest reading level,&quot; and sorts accordingly.</p>
<p>The pattern is: structured field in your content → indexed by Pagefind → listed in <code>sortable_fields</code>** with a description → the AI handles the rest. You don't write sort logic or parse the user queries. You describe your data and the AI figures out when sorting applies.</p>
<h2>Further Tuning (If You Want It)</h2>
<p>Most sites will never need to go beyond a preset and a good site description. But if you're the kind of person who tunes (and if you're reading a blog post this deep into AI search configuration, you probably are), here's what to look at for encyclopedic content.</p>
<p><code>maxPagefindResults</code> controls how many results Pagefind returns before Scolta re-ranks them. The default is 50; the catalog preset already raised it to 75, because at over 6,000 pages a query like &quot;ancient civilizations&quot; can match hundreds of articles, and a wider initial fetch gives the re-ranking stage more to work with. Pagefind is fast, so pushing it higher costs little if your corpus is bigger than ours.</p>
<p><code>aiSummaryTopN</code> controls how many top results get sent to the AI for summary generation. The preset bumped it from 10 to 15, which suits broad queries that surface relevant articles across several domains. Raise it further and the tradeoff is more latency and more tokens per summary.</p>
<p><strong>Custom stop words</strong> can help if your corpus has terms that appear everywhere but carry no search signal. For a Wikipedia corpus, you might add &quot;article,&quot; &quot;section,&quot; &quot;reference&quot;, words that are ubiquitous in encyclopedia content but meaningless for ranking.</p>
<p><strong>The admin UI exposes all of this.</strong> The Scoring section is collapsed by default (because the preset handles it), but expand it and every parameter has a numeric input with inline help text. Change a value, save, and it takes effect on the next search. No rebuild is needed for scoring changes, only content changes require a rebuild.</p>
<p>For the CLI-inclined, <code>drush scolta:status</code> shows your current configuration and index health. <code>drush scolta:clear-cache</code> wipes the AI response cache if you want to test expansion changes with fresh LLM calls instead of cached ones. By default Scolta is optimized to reuse search results if the same search is made multiple times.</p>
<p>Scolta has plenty of other knobs (multilingual expansion across 29 languages, custom AI prompts, alternate recency curves, per-element index weighting), all exposed in the admin UI. But for most sites the preset, a good site description, and maybe one or two scoring tweaks are the whole job.</p>
<h2>Keeping the Index Current</h2>
<p>When you publish, edit, or unpublish content, the search index needs to reflect those changes.</p>
<p>On Drupal, Scolta hooks into Search API's indexing system. Content changes queue automatically, and <code>drush scolta:build</code> picks them up (running the full export-then-index pipeline). For a site with frequent updates, run it on cron or trigger it from a deployment script. For a static corpus like The Athenaeum where articles don't change, a one-time build is enough.</p>
<p>If you've already exported content and just want to rebuild the Pagefind index (useful when testing scoring changes that affect index structure), <code>drush scolta:rebuild-index</code> skips the export step and re-indexes against already-exported content. Faster when the content hasn't changed.</p>
<p>Scoring changes take effect immediately, no rebuild needed. Change <code>title_match_boost</code> from 2.0 to 2.5, save, and the next search uses the new value. Only structural changes (new content, new fields in the index, and changes to the indexer configuration) require a rebuild.</p>
<h2>What Comes Next</h2>
<p>The next post in this series configures Scolta on a WordPress demo: a fictional diary of the Space Race, over 200 blog posts spanning 1957 to 1973. That content is narrative rather than encyclopedic, so the site type changes and the tunables change with it, with the same Scolta underneath.</p>
<p>A later post deploys Scolta on a medical site, where someone searching &quot;my head hurts&quot; needs to land on the right clinical terminology (&quot;intracranial hypertension,&quot; say) buried in a large body of technical content. That's the vocabulary-bridging case the &quot;Documentation &amp; Reference&quot; preset is built for, a different problem from an encyclopedia where readers already know the right words.</p>
<p>Across all of them, Scolta adapts to each platform's native patterns (Search API on Drupal, a settings-page plugin on WordPress, a config file and CLI commands elsewhere) while the presets, the scoring, and the AI underneath stay the same.</p>
<h2>Try It</h2>
<p>The Athenaeum is live at <a href="https://scolta-demo-drupal-pedia.tag1.ai/" target="_blank"> scolta-demo-drupal-pedia.tag1.ai</a>. Search for something conceptual, &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/article/metallica?q=art+born+from+suffering" target="_blank">art born from suffering</a>,&quot; &quot;<a href="https://scolta-demo-drupal-pedia.tag1.ai/article/metallica?q=animals+thought+to+be+extinct" target="_blank">animals thought to be extinct</a>,&quot; and watch what comes back.</p>
<p>Running Scolta on your own Drupal site is the same path this post walked: composer require the module, drush enable it, configure the Search API server, build the index, pick a site type, and write a good site description. For the bigger picture, <a href="https://www.tag1.com/blog/introducing-scolta/">Introducing Scolta</a> covers the project and where it's headed, and<a href="https://www.tag1.com/how-to/the-practical-path-to-ai-search/"> The Practical Path to AI Search</a> goes deep on the four-stage pipeline. Give it a try and let us know how it goes!</p>
<p>Image by <a href="https://www.pexels.com/@pppsdavid//" target="_blank" rel="noopener noreferrer">David Yu</a> from <a href="https://www.tag1.com/h[ttps://www.pexels.com/photo/glitchy-colorful-abstract-art-11774159/](https://www.pexels.com/photo/illuminated-cubes-with-dark-background-6727759/)" target="_blank" rel="noopener noreferrer">Pexels</a></p>
]]></description>
      <pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Jeremy Andrews</dc:creator>
      <guid>https://www.tag1.com/how-to/how-to-scolta-on-drupal/</guid>
    </item>
    
    
    <item>
      <title>Drupal AI Summit NYC 2026: A Community Coming Together Around AI</title>
      <link>https://www.tag1.com/blog/drupal-ai-summit-nyc-2026-recap/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
         Tag1's Creative Director/Creative Strategist, <a href="https://www.tag1.com/team/#pilar-belhumeur">Pilar Belhumeur</a>, attended the Drupal AI Summit NYC 2026, which was a full day of talks, demos, and conversations about where Drupal is headed in the age of AI. Here she shares the key themes that emerged, from Drupal's role as an AI orchestration layer to the community's commitment to building a responsible, human-centered AI future. 
    </div>
</div>
</div>
<p>The Drupal AI Summit in New York City brought together developers, strategists, designers, and agencies for a full day of talks, demos, and conversations about where Drupal will go next in the age of AI. What struck me most wasn't any single talk or demo, it was the common belief that the Drupal community needs to come together to shape this future responsibly.</p>
<p>Here are the core themes, and the talks that stood out.</p>
<h2>The Big Picture: Drupal as an &quot;AI Harness&quot;</h2>
<p>Matthew Saunders kicked off the day with a framing that anchored everything that followed: &quot;Drupal is becoming an AI harness.&quot; As organizations move AI from experimentation into practical operations, the focus should shift from <em>which</em> AI model is used to the system that orchestrates and governs those models.</p>
<p>An AI harness, as Saunders defined it, connects models to essential organizational requirements: structured data, governance, human oversight, and workflow automation. With 25 years of experience managing structured content, APIs, and permissions, Drupal is positioned to act as that operational layer.</p>
<p>Saunders also highlighted the <a href="https://www.tag1.com/ai" target="_blank" rel="noopener noreferrer">Drupal AI Initiative</a> — a community-led effort with 30+ partner organizations focused on coordinating responsible AI capabilities and avoiding vendor lock-in through community-driven innovation. A core value of the summit, repeated throughout the day, was the importance of a &quot;human × AI partnership&quot; — ensuring AI augments professional expertise rather than replacing it.</p>
<h2>What the Industry Is Aligning On: Key Themes from the Drupal AI Summit</h2>
<p>These were just a few of the ideas surfaced during the sessions:</p>
<p><strong>Vibecoding for prototypes, Drupal for systems that last.</strong> Josh Koenig cautioned that while generative AI can rapidly build websites and democratize web creation by allowing users to assemble functional interfaces without traditional development hurdles, &quot;vibe coding&quot; often lacks long-term maintainability, governance, and structural integrity. The result is frequently &quot;spaghetti code&quot; that becomes difficult for teams to manage as projects grow in complexity. This is where Drupal can shine. The takeaway: use AI to prototype fast, but use Drupal to build systems that last — sustainable, reliable, and integrated into existing professional workflows.</p>
<p><strong>Orchestrating autonomous agents.</strong> The future, according to Koenig, lies in turning AI into a teammate. Today's AI-driven web development is largely a &quot;single-player&quot; experience — just an individual talking to a computer. The next chapter is about building orchestrated workflows where multiple stakeholders collaborate within a controlled environment, ensuring consistency across a large ecosystem of websites. Drupal is well-positioned to be that orchestration layer — not just a platform with AI features, but an AI-friendly ecosystem that supports protocols like the Model Context Protocol (MCP), allowing it to act as a structured source of content and context for external AI agents.</p>
<p><strong>Context-driven AI.</strong> Without structured context, AI outputs are off-brand, non-compliant, or just generic — what Kristen Pol memorably calls &quot;AI slop.&quot; As Pol put it, context is the difference between &quot;AI that guesses&quot; and &quot;AI that gets it.&quot; Her project, the <strong>Context Control Center (CCC)</strong>, also known as the AI Context module, provides a centralized hub within Drupal to capture and manage an organization's rules, policies, and guidelines, then map them directly to AI features. Instead of relying on vague prompts or scattered style guides, CCC treats context as managed content — with familiar Drupal capabilities like revisioning, scheduling, and scoping by workflow, language, or site section. That means an organization can declare rules and have them applied consistently across every AI output.</p>
<p><strong>From UX to AX (Agent Experience).</strong> Brands now need to design experiences not just for humans, but for the AI agents acting on their behalf. As more website traffic comes from AI agents and crawlers rather than human visitors, organizations have to think about how their content, components, and APIs are &quot;consumed&quot; by machines — and how to ensure brand voice, accuracy, and trust are preserved when an agent is the one mediating the experience.</p>
<p><strong>Data sovereignty and trust.</strong> Particularly relevant for the public sector, Amazee's Jeroen Spitaels emphasized guarantees of no data retention and no training on user data as essential for public sector institutions. For governments, universities, and any organization handling sensitive information, it's not enough for AI to be powerful — it has to be trustworthy, transparent, and sovereign. That means knowing exactly where your data lives, who has access, and being certain it isn't quietly being used to train someone else's model.</p>
<h2>Where It Got Specific: Sessions Worth a Closer Look</h2>
<p>Every summit has sessions that move beyond the conceptual and get specific. These three stood out because they tackled the operational realities of actually deploying AI — and what it takes to do it well.</p>
<h3>The Actual Playbook for Deploying AI without Breaking Trust</h3>
<p>John Doyle's session on implementing AI teams and workflows was one of the day's standouts because it tackled the operational reality of AI — not just the tech.</p>
<p>His core idea: stop thinking about AI as isolated prompts and start building &quot;digital teammates&quot; — governed agents with defined owners, SLAs, and clear inputs and outputs. That's a profound reframing. A digital teammate isn't a tool you use; it's a team member you onboard, manage, and hold accountable.</p>
<p>Doyle made a strong case for Drupal as the ideal platform for AI orchestration thanks to its API-first architecture, structured content model, and workflow states. He demoed AI integrated directly into a Drupal interface, generating content drafts based on predefined project briefs and design systems.</p>
<p>His operational advice was refreshingly grounded:</p>
<ul>
<li>Give every AI agent a charter — clear scope, purpose, and boundaries.</li>
<li>Always maintain a human-in-the-loop for final verification.</li>
<li>Iterate slowly to ensure quality control.</li>
</ul>
<p>This is exactly the playbook organizations need if they want to actually deploy AI without breaking trust. At the end of his talk, I was able to speak with him, and he mentioned they had piloted this approach at Digital Polygon, and it was very successful.</p>
<h3>What a Real AI-Powered Website Experience Actually Looks Like in Practice</h3>
<p>John Tran, CTO of Image X, delivered the kind of real-world example I wanted to see. His session detailed how Drupal can be transformed from a static content management system into a dynamic, AI-orchestrated experience platform using <strong>AG-UI</strong> (Agent-to-User Interaction).</p>
<p>The technical foundation is semantic search — using embedding models and vector databases to map relationships between content, focusing on intent, context, and meaning rather than keywords. That allows the system to retrieve highly relevant information even when the user's phrasing is conversational.</p>
<p>The proof-of-concept for an advanced implementation of an AI chatbot was great to see. The system:</p>
<ul>
<li>Triggers specific tools dynamically (such as directions or live weather APIs)</li>
<li>Renders interactive SDC (Single Directory Components) directly within Drupal based on the agent's logic</li>
<li>Lets users flag content as they navigate, which the AI then aggregates into a customized, downloadable brochure or notebook</li>
</ul>
<p>Tran's future vision is for Drupal to handle the orchestration of these experiences natively — moving away from rigid, pre-built pages toward fully dynamic, component-based websites where the entire site experience is generated or assembled on-the-fly based on the user's specific goals, all while maintaining site governance and using standard Drupal form-handling workflows.</p>
<p>Just as importantly, because the AG-UI toolkit is agent-agnostic, it prevents vendor lock-in — allowing developers to switch between LLM providers or agent frameworks without rebuilding the front-end experience.</p>
<p>This is what &quot;richer interactivity&quot; looks like in practice. It's still a chatbot at its core, but with a level of engagement and contextual awareness that genuinely solves problems rather than just answering questions. This felt less like a novelty and more like a higher-order assistant doing meaningful work.</p>
<h3>Three Capabilities Your Platform Needs for an AI-First Internet</h3>
<p>The Acquia session from Martin Anderson-Clutz framed something every digital team needs to wrestle with: in the evolving role of websites and an AI-first internet, content has to go everywhere — not just to your website, but to AI crawlers, agents, and downstream experiences you don't directly control.</p>
<p>To meet that reality, Anderson-Clutz advocated for three keys to a modern DXP:</p>
<p><strong>Agile Content Engine.</strong> Beyond traditional drafting and publishing, organizations need to use AI to accelerate planning and ideation, optimize content post-publication for conversion, and embrace structured content in formats like JSON and Markdown for true omnichannel delivery.</p>
<p><strong>Robust Experience Layer.</strong> As users increasingly turn to websites to validate purchase decisions rather than for education, the site's role shifts toward frictionless brand onboarding (social proof, case studies, technical specs) and context-aware AI that keeps brand voice accurate, on-brand, and aligned with strategic objectives.</p>
<p><strong>Agent-Friendly Architecture.</strong> With non-human traffic rising, websites must be built for AI agents as primary users. That means treating &quot;APIs as the new UI&quot; — standardizing on machine-readable formats, adopting emerging agent-to-agent protocols, and designing systems that offer packageable &quot;skills&quot; or recipes for modular, autonomous agent interaction.</p>
<p>His core argument: Drupal is uniquely positioned as a leading, AI-ready platform because of its emphasis on structured content, enterprise governance, and community-driven innovation.</p>
<p>The takeaway: in a world where your content is increasingly consumed by machines before it ever reaches a human, the platforms that win will be the ones that treat AI agents as primary users — and Drupal is already there.</p>
<h2>A Community Embracing AI</h2>
<p>If one line captured the spirit of the day, it was this: <em>&quot;Drupal has quietly become one of the most AI-ready platforms available.&quot;</em></p>
<p>While much of the AI conversation centers on flashy chatbots and proprietary tools, Drupal has been steadily building exactly what AI agents need to do real work. That's not marketing spin — it's the natural outcome of 25 years of disciplined engineering around content structure, governance, and openness. The very things that made Drupal a leader in the structured-content era are the things AI agents require to operate reliably, safely, and at scale.</p>
<p>The Drupal community isn't just adapting to AI. It's quietly becoming one of the most credible, agent-ready platforms on the open web — and the summit made it clear we're just getting started.</p>
<div class="highlight-box">
<p>The conversations at the Drupal AI Summit don't end in a conference room. Tag1's <a href="https://www.tag1.com/insights/?tag=AI#selected">AI Applied Series</a> is our ongoing effort to think out loud about what responsible, practical AI looks like in real work, written by the people actually doing it. We invite you to join us there.</p>
</div>
<p><em>Bring practical, proven AI adoption strategies to your organization, let's start a conversation! <a href="https://www.tag1.com/contact/">We'd love to hear from you.</a></em></p>
]]></description>
      <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Pilar Belhumeur</dc:creator>
      <guid>https://www.tag1.com/blog/drupal-ai-summit-nyc-2026-recap/</guid>
    </item>
    
    
    <item>
      <title>Workspaces is revolutionizing Drupal Core: Unlock True Enterprise Content Management in Drupal</title>
      <link>https://www.tag1.com/conference-sessions/drupalcon-2026-chicago-workspaces/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<h2>Session Description</h2>
<p>Traditional content staging servers have had their day. This session makes the case for retiring them and shows how Drupal Workspaces does the job better.</p>
<p><a href="https://www.tag1.com/team/#peta-hoyes">Peta Hoyes</a> (Partner/COO) and <a href="https://www.tag1.com/team/#ray-stuart">Ray Stuart</a> (Senior Backend Engineer) walk through why staging sites are expensive, operationally complex, and risky for content teams. Then they show how Workspaces, stable in Drupal core since version 10.3 and shipping with every Drupal 11 install, replaces them. The architecture is straightforward: content branches that layer on top of your live site, full-site preview that reflects views, menus, layouts, and entity references in context, atomic publishing, and zero overhead on production traffic. Workspaces Extra (WSE) adds scheduled publishing, rollback, external reviewer links, access control, and configuration staging. The session closes with live demos and a look at why Workspaces is the right place to run AI content generation safely before anything touches production.</p>
<p>Note: Fabian Franz, Tag1 VP of Engineering who is a large and valued contributor to this session, was unable to join in person.</p>
<h2>What You Will Learn</h2>
<ul>
<li>Why staging servers break down at scale and what they cost</li>
<li>How Workspaces works</li>
<li>What Workspaces Extra (WSE) adds on top of core</li>
<li>How to use a workspace as a safe sandbox for AI-generated content</li>
<li>What is coming next in core</li>
</ul>
<h2>Transcript</h2>
<div class="transcript">
<p>[00:00:00] All right, let's get started everyone. We got a lot to cover and uh I will be channeling Fabian Franz.</p>
<p>[00:00:12] Um welcome to our enterprise content management um in Drupal with Workspaces talk. Um I'm Peta Hoyes. I'm the chief operating officer. Sorry.</p>
<p>[00:00:24] Into the mic. Into the mic. Into the mic. All right.</p>
<p>[00:00:27] Um I'm Peta Hoyes. I'm the chief operating officer of Tag One Consulting.</p>
<p>[00:00:32] Um I found my way to Drupal a little um via engineering product design and</p>
<p>[00:00:41] wanting to make technology uh very transparent to what I call fuzzies uh</p>
<p>[00:00:47] artists and musicians and writers. Um, back in 1993 when the web wasn't a</p>
<p>[00:00:55] thing, um, I used the Bolton board software, uh, to create a wide area network, um, for to connect people. Um,</p>
<p>[00:01:05] basically I had a wall of haze modems and a T1 line coming into a Brooklyn brownstone.</p>
<p>[00:01:12] Fast forward about 15 years.</p>
<p>[00:01:15] What I realized was that I was actually creating social networks, which I didn't know what they were at the time.</p>
<p>[00:01:25] While I try not to live in regrets, I do get serious twitches from time to time.</p>
<p>[00:01:33] Around 2008 though, Drupal got on my radar when I started working with Nat Catchpole at Civic Actions.</p>
<p>[00:01:42] Catch introduced me a few years later to Jeremy Andrews and Ryan Newton at Tag One Consulting.</p>
<p>[00:01:50] We've continued making waves ever since then, especially in the Drupal performance arena. I'm joined today by</p>
<p>[00:01:58] one of my colleagues, Ray Stewart, who will be presenting for Fabian Franz,</p>
<p>[00:02:06] who's TAG 1's VP of software engineering.</p>
<p>[00:02:10] He's recovering from the flu unfortunately and couldn't be here. So, we're going to do our best to cover for him.</p>
<p>[00:02:19] As you guys know, Drupal is a D7 maintainer and core committer and he's been developing content preview system</p>
<p>[00:02:27] CPS um the D7 predecessor to workspaces since 2014.</p>
<p>[00:02:37] He's been guiding the development of workspaces for the last three years in pursuit of feature parody with CPS.</p>
<p>[00:02:45] Now, Ray Stewart, he's a senior technical lead at TAG 1 and is an avid outdoorsman.</p>
<p>[00:02:52] We happen to both be Jamaican, by the way.</p>
<p>[00:02:56] And like the Jamaican bobsledding team, we kind of just do it, do anything just because.</p>
<p>[00:03:05] and we go for the gold.</p>
<p>[00:03:08] Ray's expertise centers on back-end architecture and development with front-end capabilities. He specializes</p>
<p>[00:03:16] in what he considers moving as moving data around in ways that create meaningful impact.</p>
<p>[00:03:23] Since discovering D Drupal around 2007, Rey has built his career on the platform, developing a particular</p>
<p>[00:03:32] appreciation for teams where each person contributes meaningfully to the complex engineering challenges.</p>
<p>[00:03:40] That is TAG One in a nutshell.</p>
<p>[00:03:43] If you're not familiar with TAG1, we're a global technology group that focuses on using open source software to solve</p>
<p>[00:03:50] very difficult problems for a lot of difficult difficult to different clients. Um, we work with a lot of</p>
<p>[00:03:58] different technologies, but we're best known as the number two all-time contributor to Drupal. Since 2001, our</p>
<p>[00:04:07] team has created many of the innovations that have fueled Drupal success, such as the taxonomy system and migrate API. If</p>
<p>[00:04:15] you use Drupal, and I'm pretty sure you have, um, every time you update a Drupal site, we manage the infrastructure of drupal.org.</p>
<p>[00:04:26] um and uh this and basically the tooling and uh use to build test and release Drupal on behalf of the Drupal association and its community.</p>
<p>[00:04:38] So what do we mean by true enterprise content management?</p>
<p>[00:04:44] Since TAG one was founded, we've worked with a lot of enterprise organizations</p>
<p>[00:04:50] over those last few decades. We've helped many grow their websites from something basic to a critical part of their business.</p>
<p>[00:05:00] As those websites have become more and more important, the most successful enterprise companies have expanded the</p>
<p>[00:05:08] role of their CMS from a tool that helps them manage their website to one that's also a key element of content governance.</p>
<p>[00:05:19] If content governance isn't a familiar term or concept to you, it's a term that refers to all of the various things from</p>
<p>[00:05:28] procedures to technology that a complex organization needs to make sure its content meets its standards and requirements.</p>
<p>[00:05:38] Anyone who works for or with large enterprise organizations knows that that's no small feat.</p>
<p>[00:05:47] There can be a lot of content stakeholders with diverse concerns from brand marketing to legal compliance.</p>
<p>[00:05:54] That's a lot of cooks in the kitchen.</p>
<p>[00:05:57] Strong content governance is what hopefully stops all of that from devolving into chaos.</p>
<p>[00:06:04] So for CMS to truly meet the requirements of an enterprise content management, it needs to provide the tools for both creating content and</p>
<p>[00:06:13] managing the end toend governance of that content. Drupal has been working towards this in core for a long time</p>
<p>[00:06:20] now. From early features like granular user roles that let enterprise teams effectively control permissions and</p>
<p>[00:06:29] access to incorporating tools like workflows and content moderation in D8 for managing the content of publishing life cycles.</p>
<p>[00:06:39] And with the adoption of workspaces and core, we've closed another major governance gap where Drupal has lagged</p>
<p>[00:06:46] for some time now. Content staging and review in the CMS.</p>
<p>[00:06:52] Let me explain why that's more important than ever.</p>
<p>[00:06:56] In our modern age of com com composable content and visual page builders, a lot</p>
<p>[00:07:03] of enterprise websites have gone from looking like this to more like this. These are types of</p>
<p>[00:07:11] sites we've worked hard to make Drupal great at building.</p>
<p>[00:07:16] Content is coming from a bunch of different systems. It's highly designed and dynamically laid out in the CMS with</p>
<p>[00:07:25] edits and updates automatically propagating all over the site. But here's the catch.</p>
<p>[00:07:34] Those types of critical large-scale updates that Drupal does so well, things like adding new products or services,</p>
<p>[00:07:41] migrating in a new department, those are notable visual changes. They impact navigation and views and blocks throughout the site.</p>
<p>[00:07:51] And because of that, the governance of those changes, the reviewing of things like brand consistency, legal compliance, accessibility, whatever</p>
<p>[00:08:00] really need to happen in context of the overall site with all the pieces of the puzzle present. Until now, Drupal hasn't</p>
<p>[00:08:09] had a great solution for that kind of content staging. its required workarounds and compromises. Things like the content staging site.</p>
<p>[00:08:21] The content staging site is of course a duplicate of your production site where content managers can stage all of their</p>
<p>[00:08:29] changes without dup disrupting the live site. Sounds great in theory and a lot of you are using them for enterprise websites, but it's time to say goodbye.</p>
<p>[00:08:41] Here's why.</p>
<p>[00:08:43] That staging site requires a duplicate codebase, database, file system that has to be maintained. Often that means a</p>
<p>[00:08:52] whole other file server that requires ongoing maintenance.</p>
<p>[00:08:58] Keeping it in sync with your live site probably means complex CI/CD pipelines and build workflows all need to be used.</p>
<p>[00:09:08] Sure, I get it. We're all working in the cloud.</p>
<p>[00:09:12] our DevOps teams or our favorite manage platform has a lot of stuff set up for us. But getting all that stuff set up, keeping it up to date is time consuming.</p>
<p>[00:09:23] It's expensive. And for what?</p>
<p>[00:09:28] Staging infrastructure often doesn't have resources equal to production, which introduces concerns when it comes to things like performance testing.</p>
<p>[00:09:39] But okay, we're enterprise drupalists.</p>
<p>[00:09:43] We're comfortable with DevOps coding, config management. What needs to be done? What about the impact though on the people who need to manage the content?</p>
<p>[00:09:53] External staging sites mean content managers need to work in two or more different places.</p>
<p>[00:10:02] Why more?</p>
<p>[00:10:04] Well, what happens when you need to be staging more than one set of updates at a time?</p>
<p>[00:10:11] More staging equals more problems.</p>
<p>[00:10:14] I know the build tubes can probably handle it, but do you know what most content managers don't love? Remembering</p>
<p>[00:10:21] which subdomain that's autogenerated from the git branch ID is the only one they're supposed to be updating.</p>
<p>[00:10:29] But even if that doesn't convince you, the hardest part and the part you've been waiting for me to get to this entire time,</p>
<p>[00:10:38] you need some way to get the content from one site to another.</p>
<p>[00:10:44] Content deployment across systems is rife with opportunities for failure.</p>
<p>[00:10:50] Remember, we're not talking about version controlled code.</p>
<p>[00:10:55] We're talking about rows of data from multiple tables moving from one database to a second completely unrelated database and that is always risky.</p>
<p>[00:11:09] There have of course been numerous attempts to overcome this in the Drupal community over the years. I'm certain I'm not the only one who remembers and</p>
<p>[00:11:17] still has nightmares about the features module and long lists of overridden mistakes.</p>
<p>[00:11:23] Seriously though, drupal.org's documentation has a page that compares 50 content deployment modules and there</p>
<p>[00:11:31] are also a number of external systems that tackle a problem. Some of these solutions are quite good. But to be</p>
<p>[00:11:38] clear, no matter what, content staging servers are costly,</p>
<p>[00:11:46] complex, very inconvenient, and very risky way to work around Drupal's historic limitations and workspaces.</p>
<p>[00:11:57] It's incorre.</p>
<p>[00:12:09] Okay.</p>
<p>[00:12:14] So uh you've heard about uh all these capabilities the atomic publishing full-sight preview zero overhead on</p>
<p>[00:12:23] production uh which might sound complex to set up it's not really uh workspaces is now built into Drupal core uh getting</p>
<p>[00:12:32] started is uh one command that many of us are familiar with uh drush enable workspaces and you're done mostly as</p>
<p>[00:12:41] with a lot of Drupal mostly uh But no, you have uh what's in core and you also have WSE. WSSE is Workspaces Extra. Uh</p>
<p>[00:12:51] since uh Drupal 10.3 in June 2024, Workspaces is stable and fully supported. Uh it ships with every Drupal</p>
<p>[00:13:00] 11 installation. Uh and you now have enterprise content staging that rivals Adobe Experience Manager and it's free</p>
<p>[00:13:07] and open source. Uh but you really want also workspaces extra uh which WSE</p>
<p>[00:13:15] uh core workspaces is git branches for content. WSE is like the full CI/CD pipeline uh that you have for your code.</p>
<p>[00:13:25] Uh you have scheduled publishing, preview links for external stakeholders, roll back capability, access control,</p>
<p>[00:13:32] menu staging and task monitoring. Uh plus WSE config lets you stage configuration changes alongside content.</p>
<p>[00:13:43] Uh installing WSE um is also should be very familiar to many of us now which is just a composer require uh Drupal WSSE.</p>
<p>[00:13:53] Uh and that's pretty much it. Um it's a pretty low barrier to entry and enterprise capability out of the box.</p>
<p>[00:14:06] Workspaces in a state is a staging site.</p>
<p>[00:14:09] So traditional staging servers cost as much as production. Uh separate infrastructure, databases, certificates,</p>
<p>[00:14:16] you typically typ typically get one maybe two environments and workspaces eliminates all of that or can eliminate</p>
<p>[00:14:23] all of that. Uh uh when you preview a workspace, you see the entire site as it will appear when it's updated and live</p>
<p>[00:14:32] uh and published. Uh views, listings, navigation menus, entity references, everything reflects your stage state. Uh</p>
<p>[00:14:41] your workspace changes layer transparently on top of the live site.</p>
<p>[00:14:45] Uh uh it's a complete context and automatic.</p>
<p>[00:14:49] Uh you get enterprise features uh out of out of workspaces right from the get-go.</p>
<p>[00:14:54] Uh unlimited workspaces uh design, content, translation and legal teams uh whichever teams you have can all work in</p>
<p>[00:15:02] parallel. Each workspace is isolated with granular permissions. Content moderation integration uh which is</p>
<p>[00:15:09] somewhat in development but but ongoing uh gives you entity level workflow states with atomic publishing and schedule p schedule publishing roll back</p>
<p>[00:15:18] external preview notifications all within one Drupal installation. Uh zero infrastructure cost zero overhead on</p>
<p>[00:15:25] live traditional stage is infrastructure whereas workspaces is architecture.</p>
<p>[00:15:33] Now most of us here know git. Uh if you do uh then you you pretty much already understand workspaces. Uh the workflow</p>
<p>[00:15:41] is uh identical to working on a feature branch and then merging to main. Uh the difference here is content. So in git uh</p>
<p>[00:15:49] you create a branch where changes accumulate. Uh you switch between main and your branch to see uh the complete</p>
<p>[00:15:56] state. Uh then merge atomically. The the parallels are are fairly exact. A workspace is a content branch. Live is</p>
<p>[00:16:04] main. Publishing is merging uh your staged content. Uh your repository is</p>
<p>[00:16:11] your site. The mental model is is is pretty identical.</p>
<p>[00:16:17] Uh developers take branching for granted. Uh content editors face a similar problem. uh coordinating related</p>
<p>[00:16:25] changes, but they've been stuck with per item drafts and manual coordination until now.</p>
<p>[00:16:32] Workspaces doesn't implement automatic merging uh that leads to data corruption uh with structured content. Instead,</p>
<p>[00:16:40] teams should coordinate via communication uh shared workspaces for collaboration and for clear content ownership. Your workspace will never</p>
<p>[00:16:48] contain content that you didn't put there.</p>
<p>[00:16:53] Uh here's uh uh kind of a direct mapping of of comparing workspaces and git. Uh the repository equals the Drupal site.</p>
<p>[00:17:02] Uh git branch equals workspace. Uh the main branch equals uh the main branch in git equals the live workspace uh in</p>
<p>[00:17:12] Drupal. Uh a commit equals a save. Uh a working copy equals workspace preview.</p>
<p>[00:17:19] uh emerge equals publish.</p>
<p>[00:17:23] Uh the workflow is is identical. Create a branch for your feature, make commits, preview the complete state, get</p>
<p>[00:17:31] approval, merge to bane. Uh in workspaces, it's create a workspace for your campaign, save changes, preview the</p>
<p>[00:17:38] complete site, get approval through content moderation, and then publish.</p>
<p>[00:17:44] Developers already know this workflow intimately. content content teams just haven't had uh the tooling until now.</p>
<p>[00:17:53] Branches aren't just for de developers.</p>
<p>[00:17:55] Anytime you need to coordinate multiple related changes, preview them together and deploy as a unit, you need branching. The the architecture that</p>
<p>[00:18:04] powers git, isolated workspaces, atomic merges, zero cost on maintain, the same architecture powers workspace workspaces.</p>
<p>[00:18:12] Uh and you can say if your dev team uh wouldn't ship code without branches um then why why would your content team ship content without workspaces?</p>
<p>[00:18:24] Uh workspaces is built for content teams uh that are coordinating multiple related changes. Uh so large organizations where departments work in</p>
<p>[00:18:32] parallel uh let's say marketing legal design content uh teams uh campaign launches where articles media navigation</p>
<p>[00:18:40] and landing pages must deploy simultaneously uh what's the common thread full-site content preview. If you've ever had a landing page go live</p>
<p>[00:18:49] before its menu length you need workspace.</p>
<p>[00:18:53] Uh who it's not for? Uh here's a surprising answer. No one is</p>
<p>[00:19:00] excluded from using workspaces. Content moderation will use workspaces soon. Uh there's one issue left uh in court to</p>
<p>[00:19:06] deal with that. Um and there's an issue uh there is an issue with updating the content to the multiple workspaces. Uh there is a parallel workspaces module to</p>
<p>[00:19:15] help with that. Uh but there is also an issue that will help fix this in core as well.</p>
<p>[00:19:23] Uh workspaces has a fundamental uh principle. Um it is uh it's a boundary</p>
<p>[00:20:00] production. When the active workspace uh is live 100% of the which is 100% of the time for anonymous users uh the workspace system is literally invisible.</p>
<p>[00:20:10] Your st your staging work cannot affect production performance.</p>
<p>[00:20:15] Uh form submissions within a workspace are prohibited. Uh they trigger irreversible side effects like um uh sending emails or processing payments.</p>
<p>[00:20:25] Uh, you can stage content, preview layouts, test navigation, but can't accidentally trigger real world</p>
<p>[00:20:32] consequences. Nonworkspace safe entities that don't support revisions simply can't be modified in a workspace. The</p>
<p>[00:20:39] system enforces architectural safety at every level. Uh, now the uh how about performance?</p>
<p>[00:20:48] Uh, this is workspace's most important architectural property. uh zero overhead on the live site when the active workspace is live 100% of the time.</p>
<p>[00:20:58] Again, for anonymous users, uh the workspace system is literally invisible.</p>
<p>[00:21:03] So, your production site performs exactly as if workspaces didn't exist.</p>
<p>[00:21:09] Uh this makes workspaces viable on the highest traffic production sites in the world. Content staging systems that add</p>
<p>[00:21:18] overhead to every page load get disabled. Workspaces was architected from day one in CPS in 2014 uh to have</p>
<p>[00:21:27] zero impact on live traffic. That's why it's been running at Alexa top 500 companies for over a decade</p>
<p>[00:21:35] inside a workspace. Yes, there's overhead uh but that's generally uh editorial traffic uh which will be a tiny uh tiny fraction of your load. Uh</p>
<p>[00:21:43] you will see a slight um uh decrease in performance with workspaces because of the additional overhead. Um but the</p>
<p>[00:21:50] editors get a full site preview uh and get uh visitor while the visitors are getting the full production speed.</p>
<p>[00:21:59] The most common question is uh what happens when two people edit the same content in different workspaces?</p>
<p>[00:22:10] Uh the answer is simple and deliberate.</p>
<p>[00:22:12] Uh you can't and this is the right design. Now uh there is work as I mentioned with parallel workspaces to uh</p>
<p>[00:22:20] improve on this uh and also in core um and also uh there's no automatic merging</p>
<p>[00:22:28] uh that happens uh within workspaces uh that it's really up to the team to communicate those changes um and uh to</p>
<p>[00:22:35] merge to to manually merge those conflicts later on. Uh when an entity is modified in a workspace, uh it's tracked</p>
<p>[00:22:42] there. Uh another editor can't modify it in a different workspace. Uh this isn't a limitation exactly. It's a feature. Um</p>
<p>[00:22:50] automatic merge of structured content fields, entity references, media relationships, uh that could lead to</p>
<p>[00:22:57] data corruption. So um every system that's attempted, it has encountered fundamental issues. So especially when you're talking about config, you'd want</p>
<p>[00:23:05] to be careful with those merges. Uh, workspaces takes the same approach as a well-run dev team. Coordinate via</p>
<p>[00:23:12] communication. So, shared workspaces let teams work together. Content ownership keeps responsibility responsibilities</p>
<p>[00:23:20] clear and WSE workspaces extra lets you move content between workspaces when needed.</p>
<p>[00:23:30] The absence of automatic merging in that regard is a safety guarantee. Your workspace will never contain content that you didn't put there.</p>
<p>[00:23:42] Uh because workplace workspaces are conflict-free, scheduled publishing is</p>
<p>[00:23:48] actually safe. So WSCuler lets you set a date and a time. Your workspace publishes automatically at 2 a.m. on launch day, for example. Uh the workspaces contents are deterministic.</p>
<p>[00:24:00] What you reviewed and approved is exactly what goes live. No surprises. uh in a mergebased system you couldn't</p>
<p>[00:24:07] guarantee what state the content would be in at deployment time. Uh roll back is guaranteed to work uh because the</p>
<p>[00:24:15] workspace is an atomic unit. Uh WSC's roll back feature can revert an entire published workspace. Uh undo the</p>
<p>[00:24:24] complete deployment. Uh there's a clean boundary. These specific changes uh came from this specific workspace. No</p>
<p>[00:24:31] ambiguity about what to undo. uh revert the workspace and you're done.</p>
<p>[00:24:38] And conflict free means correctness to some degree. Uh what your editor reviews is what is published. What your legal</p>
<p>[00:24:47] team approves via preview link is what goes live. Uh no merge during during deployment, no conflicts introduced during review.</p>
<p>[00:24:57] Now uh the deletion problem and solutions. Uh deleting content in a workspace creates a challenge. Uh the</p>
<p>[00:25:06] content needs to disappear from uh the workspace preview but remain visible on the live site until you publish. Uh trash solved this elegantly with a uh deleted field using a a turnary state.</p>
<p>[00:25:19] Uh essentially uh the values greater than one are Unix timestamps of actual deletion. Um and th those are subject to</p>
<p>[00:25:27] garbage collection as well. Um uh one means draft in a in a workspace not yet published and zero means live and visible.</p>
<p>[00:25:35] When you delete content within a workspace, the entity remains live in the main table with deleted equals zero.</p>
<p>[00:25:42] But the workspace revision marks it as deleted. Within the workspace, that content disappears from listings, views,</p>
<p>[00:25:49] and search results. Uh on the live site, it remains visible until you publish.</p>
<p>[00:25:55] Content deleted in a workspace is recoverable until you publish. Uh and this</p>
<p>[00:26:02] integration removes the uh the constraint. Any entity type can now participate in workspaces,</p>
<p>[00:26:10] not just those with publication status, by the way.</p>
<p>[00:26:16] Uh workspace and workflows. Uh workspaces integrates cleanly with with Drupal's uh core workflows module. Uh</p>
<p>[00:26:24] you can move an entire workspace through workflow states draft, review, approved, published, for example. Uh the entire</p>
<p>[00:26:32] collection of changes moves through the approval process as a unit. Uh your legal team reviews the complete workspace for example and your content</p>
<p>[00:26:40] manager approves the entire set and when it reaches publish state, the whole workspace deploys a auto atomically.</p>
<p>[00:26:48] The simple the simplified content workflow initiative makes this uh even better. Um uh when those final issues land uh it</p>
<p>[00:26:57] will um make content moderation and workspaces work work together seamlessly for complex scenarios where different entities need different approval paths.</p>
<p>[00:27:08] Entity workflow uh entity workflow module contrib module provides entity level workflow granularity within a workspace context. Most sites won't need this uh but the capability exists.</p>
<p>[00:27:24] Uh now beyond content entities, uh here's where workspaces becomes truly revolutionary. Uh up until now we've</p>
<p>[00:27:31] talked about content entities, uh nodes, media, taxonomy terms. Uh but what about configuration? Uh block placements, view</p>
<p>[00:27:40] configurations, theme settings, site title, field definitions. Uh WSC config is bringing all of that into workspaces</p>
<p>[00:27:48] and it's almost production ready. Uh with WSC config you can modify a view for example within a workspace and</p>
<p>[00:27:55] preview it uh change sorting and filters all stageable all previewable block placements menu settings layout</p>
<p>[00:28:03] configuration site settings everything that's normally configuration management export import workflow uh can now happen within a workspace. Uh there are some</p>
<p>[00:28:12] limitations that I I can't talk about right now. I'm not I'm not sure exactly but there are some limitations but it's almost there. uh you add WSE theme and</p>
<p>[00:28:20] and you can preview an entirely different theme. Want to test a major visual redesign? Stage it in a workspace, preview it with real content,</p>
<p>[00:28:29] get stakeholder sign off via WSC's preview links, then publish the entire redesign atomically.</p>
<p>[00:28:40] Configuration staging within workspaces closes one of the last remaining gaps between Drupal and the most capable proprietary CMS's.</p>
<p>[00:28:52] Uh and now like AI is uh everybody's talking about AI and uh no difference here. Workspaces is the perfect AI</p>
<p>[00:29:00] sandbox. Um AI content generation is scaling rapidly. The Drupal AI module integrates</p>
<p>[00:29:08] uh 48 plus um AI providers with uh automators that bulk populate fields, generate summaries, translate content.</p>
<p>[00:29:18] But AI is non-deterministic. It hallucinates. Uh it will produce errors.</p>
<p>[00:29:23] So how do you use AI at scale safely? Uh workspaces provides the natural architecture, the side effect boundary.</p>
<p>[00:29:31] Think of a workspace like a a pure function. Everything inside is safe, reversible, no side effects.</p>
<p>[00:29:39] Uh, and AI can generate content, edit fields, restructure pages, populate entire sections, all inside a workspace.</p>
<p>[00:29:46] But nothing touches live, completely reversible. Uh, publishing uh is IO at that point. That's when content gains irreversible real world effect.</p>
<p>[00:29:59] Uh the human uh however you still want the human in there to verify everything, right? So uh the human can verify everything within the workspace uh before it's uh published to live.</p>
<p>[00:30:12] And the the diff in the workspace uh will uh will help you to accomplish that.</p>
<p>[00:30:23] Now uh workspaces uh as I mentioned is stable in core uh since 10.3 and it ships with uh every Drupal 11</p>
<p>[00:30:31] installation. Um it's no longer experimental. Uh it's core infrastructure. Uh so it's say it's safe to use them. Uh your sites can benefit</p>
<p>[00:30:40] from it now. Uh but here's what we need from the community. Uh this is the X uh testing uh real world testing on</p>
<p>[00:30:49] production projects. Try it with your contrib modules. Uh test it with custom entity types. Uh report bugs when you</p>
<p>[00:30:56] find them. Uh like any complex subsystem, there are edge cases. Uh every bug report makes it better. Every production deployment proves the</p>
<p>[00:31:04] architecture. Uh the people who built this have been running in production since 2014. Now it needs the community.</p>
<p>[00:31:13] Uh this matters urgently. uh AI uh workspaces isn't just nice uh to have for AI integration. It's almost a</p>
<p>[00:31:21] must-have. Uh AI content generation is scaling now, but with the problems with, you know, hallucination and uh you know,</p>
<p>[00:31:30] you you could really use a sandbox and workspaces provides that sandbox for your AI content generation.</p>
<p>[00:31:40] Okay. So, now we're getting into uh demos. Uh I'm going to</p>
<p>[00:31:48] start up a couple videos that we have here and I'll try to talk over them. Uh Fabian couldn't be here, but uh we have</p>
<p>[00:31:56] um couple of items that would be helpful to see. Let's see.</p>
<p>[00:32:08] So this is uh we're sw this is an example of switching into a workspace</p>
<p>[00:32:15] and uh we're looking at some content and editing uh various aspects of it including uh the title</p>
<p>[00:32:24] uh and the body content uh and this is the stage uh workspace.</p>
<p>[00:32:37] Now we're going to save that content in the stage workspace. So now you can see that even better uh is the change that</p>
<p>[00:32:45] we made to the title. So we're looking at that in the stage workspace. Once we switch back to live, then you see that it switches back to super easy, which is</p>
<p>[00:32:53] the original title. Uh back to the stage content type. And now we're going to uh edit the content again and change a few more items.</p>
<p>[00:33:05] Uh we're adding some we're updating the taxonomies and we're going to change out um an image.</p>
<p>[00:33:20] And we're also going to change some content in a multiple uh text field.</p>
<p>[00:33:35] And maybe most importantly and interesting, we're going to change the URL alias uh for this item as well. And</p>
<p>[00:33:43] so you'll see even- better as part of the alias.</p>
<p>[00:33:50] Uh it's hard to see, but you'll up in the URL bar there, it's even better as the alias. And you'll see that the uh</p>
<p>[00:33:58] contents have been switched out. Uh the image has been switched out and uh as well as the terms that we had in there.</p>
<p>[00:34:05] Now we're going to switch back to live.</p>
<p>[00:34:07] And you'll see that the the URL has switched back to the original URL. Uh and the image has been switched back as well, which are the easy visual marks.</p>
<p>[00:34:18] Uh now we're going to switch back to the stage side and we're going to go to</p>
<p>[00:34:24] layout. This using layout builder. Uh so now we're going to make a change here</p>
<p>[00:34:31] which is uh we're going to move some blocks around. We'll put the ingredients on the right and the uh directions on the left.</p>
<p>[00:34:45] And then we'll save that layout. And then we're still on the stage workspace.</p>
<p>[00:34:49] So you see that ingredients is on the right. Uh we'll switch back to the live works work workspace and you'll see that</p>
<p>[00:34:56] ingredients is on the left and back on the right again in the in the stage workspace.</p>
<p>[00:35:10] Now we're going to update uh a block and edit some of its configuration.</p>
<p>[00:35:17] We're going to change the uh the title there text field and we're also going to</p>
<p>[00:35:24] change the uh the the URL alias alias that we're pointing to</p>
<p>[00:35:32] and also changing out an image within this block as well. So keep in mind this isn't just um node content. It's uh</p>
<p>[00:35:42] entities many entities site wide any many many content entities site wide. So now we're in the stage</p>
<p>[00:35:50] workspace and we're switching we're going to the uh the workflow</p>
<p>[00:35:59] management. You can see what the the changes that are staged. And in that edit menu you can see you can move to another workspace um translate etc. So</p>
<p>[00:36:06] you have some functionality with of workspaces for the individual content items. Now we are publishing uh the</p>
<p>[00:36:13] content and you'll see that uh the changes are uh now applied in the live workspace.</p>
<p>[00:36:28] Okay. Now, we're going to create a a new workspace, a blog workspace.</p>
<p>[00:36:37] And uh this is going to go through and it's going to create um a number of uh</p>
<p>[00:36:44] blog content items uh relatively quickly. Uh it's going to be sped up here.</p>
<p>[00:37:09] So, we've created a number of items and it's all in the blog workspace that</p>
<p>[00:37:16] you can see in the upper right. And now we're going to go to uh menu items uh which is always interesting to uh figure</p>
<p>[00:37:24] out when deploying uh new content and new new menu items.</p>
<p>[00:37:29] It's creating a uh I think it's nutrition blog. Yeah, nutrition blog.</p>
<p>[00:37:35] This is when you realize that you kept on your computer glasses and not your distance glasses.</p>
<p>[00:37:42] Um okay.</p>
<p>[00:37:46] And so in the blog workspace, you can see that the nutrition blog menu item is there and takes you to the correct place</p>
<p>[00:37:52] where you would expect to be taken. Um, and go back to the homepage. We uh switch back to the live site and you'll</p>
<p>[00:38:01] ee that the uh nutrition blog menu item is no longer there.</p>
<p>[00:38:11] Switch back into returns.</p>
<p>[00:38:16] And you can see everything outstanding at a glance what is what is outstanding in in this uh workspace. And now it's going to publish uh the blog workspace.</p>
<p>[00:38:25] Uh it's published. It automatically uh directs to live and um the nutrition blog is now there. Uh you haven't done</p>
<p>[00:38:34] anything particularly uh special in order to make it happen. Um it's just there.</p>
<p>[00:38:42] Okay.</p>
<p>[00:38:50] Now, we have another video that I'll play here.</p>
<p>[00:38:59] Um, we've just And this one,</p>
<p>[00:39:08] I used a little AI, just a small one, um, make the size a little bit more.</p>
<p>[00:39:17] Oops.</p>
<p>[00:39:25] Okay. So, this one is uh is using the umami theme, but it's been replaced, as you can see, with lots of Drupal cons for Drupal Con.</p>
<p>[00:39:34] And uh we have everything in a uh in a workspace. I keep wanting to call it change set from CPS.</p>
<p>[00:39:54] And this is content that was created by an AI agent, for example. Uh we're not demoing that today, but uh this content was created by uh an AI agent and um</p>
<p>[00:40:04] just replaced lots of content uh in the site with with Drupal Con essentially and replaced a lot of terms on the site with Drupal uh Drupal Con. Uh so it's</p>
<p>[00:40:13] it's sort of AI going rampant uh in a sense. Um when you are uh looking at uh</p>
<p>[00:40:21] the the workspace uh you can see you know what's been changed what's existing uh in your AI agent workspace lots of</p>
<p>[00:40:29] changes currently existing in there and you'll see that lots of media images</p>
<p>[00:40:38] uh but you also have configuration updates uh listed in the bottom as well uh that's using WSC config</p>
<p>[00:41:04] Okay.</p>
<p>[00:41:06] And the the important thing to uh to witness or to uh realize is that it's just content that has been uh majorly</p>
<p>[00:41:15] updated. Uh that's a view that's been updated as well. Um uh with uh additional configuration in the uh in</p>
<p>[00:41:24] the change set in the workspace and uh this is showing where the change is happening uh for that.</p>
<p>[00:41:35] Okay, we have one more. Oops.</p>
<p>[00:41:56] and this one.</p>
<p>[00:42:05] Okay, so these are changes uh that we're putting in uh for the</p>
<p>[00:42:12] uh edits again that we're putting into uh content in the title and in the uh uh descript or the body.</p>
<p>[00:42:29] And this is moderated content. This change is moderated content. Um so this change was put in uh it's moderated content and then um it's been</p>
<p>[00:42:38] essentially imported into uh a workspace using what we're calling virtual workspaces uh at the current time. Um,</p>
<p>[00:42:46] so it's like using normal uh content moderation uh but now it's being imported into a virtual workspace. So</p>
<p>[00:42:53] you can imagine all these disperate changes on your site uh that were made with content moderation and then bulked into a uh virtual workspace.</p>
<p>[00:43:07] So uh yeah, we've talked a lot about workspaces and seen some cool demos. Um uh but in summary uh it's stable and</p>
<p>[00:43:16] it's in core. Uh it's fast and simple to use. Um it's the perfect AI sandbox and a and a must-have for a safe AI</p>
<p>[00:43:25] integration. Um it'll provide seamless support for content creators. um and has uh enterprise support for content uh</p>
<p>[00:43:33] workflows and governance and and again yeah so we need you uh to</p>
<p>[00:43:44] uh start using it uh workspace is stable it's ready uh test it use it report what you find uh the AI age needs this</p>
<p>[00:43:52] infrastructure now and I think we have some maybe have some time for questions</p>
<p>[00:44:06] Okay. Any questions out there? Right there. Canvas. I'm sorry. Heard a lot about Canvas.</p>
<p>[00:44:14] Canvas.</p>
<p>[00:44:15] You've heard a lot about Canvas. Uh you've heard a lot about canvas. Does it work with Canvas? Um I I would say I'm not sure. Uh but I believe that it it's</p>
<p>[00:44:24] it's a little bit different implementation and I think it's going with layout builders versus the canvas approach. I don't know enough about canvas uh to talk about that um specifically. Is Chris here.</p>
<p>[00:44:37] Okay.</p>
<p>[00:44:38] It will that is what it will.</p>
<p>[00:44:42] Yes, it will.</p>
<p>[00:44:43] What I have we've been discussing the last couple of days. But yes, it will.</p>
<p>[00:44:48] Okay. Any other questions back there?</p>
<p>[00:45:04] Uh the question is if you have a CDN or varnish cache, will it work with that or does it need something extra? Did I get</p>
<p>[00:45:10] that right? Okay. Um the it will work uh like in terms of your anonymous content</p>
<p>[00:45:17] uh that's going out. it's it's there's no change to your anonymous site usage.</p>
<p>[00:45:22] Um, in the same way that uh you're not likely to have a lot of that enabled for your uh content editors, um it it it'll</p>
<p>[00:45:30] work in the same sort of way. So, um as long as you have that set up and appropriate for your content editors using the site, then it should work. It should work fine.</p>
<p>[00:45:40] Sure. Right back there.</p>
<p>[00:45:44] How do you deal with uh giving permissions to you don't want to give any other permissions to observe the work is happening work.</p>
<p>[00:45:53] Uh the question is how do you deal with permissions uh to users that you do not want to see uh or or do not want to see</p>
<p>[00:46:01] happening in workspaces work going on there?</p>
<p>[00:46:03] Let's say I want to have someone review it before we publish it. How do you can you control the permissions for public otherwise anonymous?</p>
<p>[00:46:13] Got it. Yes, you do have uh so uh do you do you have can you give permissions or how do you deal with permissions uh for</p>
<p>[00:46:21] pages that you might want to send uh that would be for anonymous users to review? Okay. Um, so in that case, uh, I</p>
<p>[00:46:29] don't know enough about this, but I believe that there is preview functionality that you can send for reviewer links. That would be, um, I'm</p>
<p>[00:46:37] not sure if they would be, uh, that's right.</p>
<p>[00:46:41] That is right. That would be for anonymous users uh, to to click on and use.</p>
<p>[00:46:46] So you can share the preview link and in the permissions you have to select which users can access the preview. So that so</p>
<p>[00:46:54] basically what people do is create a new role specifically to trigger this and then just assign them permission share</p>
<p>[00:47:02] the link and that okay uh so uh did you catch some of that?</p>
<p>[00:47:08] Okay. So uh essentially you would uh uh prepare roles uh for users and uh create</p>
<p>[00:47:15] preview links that would then be applied for those users as well. Um would that work for anonymous as well? Yep. And that works for anonymous as well.</p>
<p>[00:47:24] Okay, any other questions? Yes.</p>
<p>[00:47:27] Yeah. Question about the static files that you uploaded inside the workspace.</p>
<p>[00:47:33] Is it available then if it's public to users that know the URL?</p>
<p>[00:47:40] Okay. So static files that are uploaded within the workspace are is that um available to uh users if they if anonymously if they know the URL?</p>
<p>[00:47:51] Okay. Uh I believe that that would be caught by Drupal in any case. However, um well, I'm not entirely sure about that. Static files uploaded in the workspace.</p>
<p>[00:48:01] Yeah. So I upload a new image. Yeah.</p>
<p>[00:48:03] It's like one JPEG and then somebody just goes anonymously to sites default</p>
<p>[00:48:09] files one. Will they see it? I think in the case of an image, so uh if if an image for example is uploaded to Drupal</p>
<p>[00:48:18] and that that file exists at a a static URL at that point kind of um would it be seen for somebody that knows the URL? I</p>
<p>[00:48:25] believe that Drupal would catch catch it in that case. Uh like uh if you're uploading a file to a workspace, it's</p>
<p>[00:48:33] still going to be covered under Drupal security and and features. So if you have it so that it's uh generally open to the world, then it would be would be</p>
<p>[00:48:41] seen. But um uh that should be caught by Drupal and Workspaces permissions I believe. Anybody know the answer to that specifically?</p>
<p>[00:48:50] Not sure. Okay. Any other questions? Right back there.</p>
<p>[00:48:59] So for for changes like the demo for views, how does that how does that interact with like the configuration</p>
<p>[00:49:07] export system? Does it like exclude those views?</p>
<p>[00:49:15] That's a good question. So the question was with the uh config with the WSC config that we showed for example the</p>
<p>[00:49:21] changes to config um that were in the demos uh does that how does that interact with the uh configuration import and export uh system in Drupal?</p>
<p>[00:49:32] Um straight up answer is I'm not sure.</p>
<p>[00:49:35] However, what I imagine would happen is that is that once it's uh once it's published, then you would have the</p>
<p>[00:49:42] ability to then export that uh to configuration in the same way. Uh that it wouldn't be um uh that whatever is in</p>
<p>[00:49:50] the live would be what would be exported. Uh however, that's a good question. Uh and I and I'm not 100% sure on that answer, but that's sort of what</p>
<p>[00:49:58] makes sense to me based on what I know there. Um, and so I think that's a that's an interesting question though because we use configuration import and</p>
<p>[00:50:05] export for a lot of things but um this might be a case where um uh for a lot of sites that are not necessarily using configuration import and export that are</p>
<p>[00:50:13] handling their configuration directly on the site and not necessarily exporting it to code all the time.</p>
<p>[00:50:20] You know followup question if I created the workspace and then after I did some changes to view for example and then in</p>
<p>[00:50:28] the live workspace I did configuration import already from view changes how then it will know what's what is the map</p>
<p>[00:50:37] now you're getting into different territory territory here so you're talking about the question is uh the configuration</p>
<p>[00:50:45] um uh if you import configuration Let's say before you publish a workspace, how does it know uh what the</p>
<p>[00:50:52] changes are? Um and I think at I think at this point the uh the answer would be</p>
<p>[00:50:59] that the configuration that is in the workspace would I believe override the configuration that was imported.</p>
<p>[00:51:08] However, I'm not sure if there would be a flag telling you that that that has been changed uh at some point.</p>
<p>[00:51:15] Yes.</p>
<p>[00:51:17] So similarly when you were talking about the the content free um aspect of content editing like let's say you have</p>
<p>[00:51:25] a a new section being added or a new campaign you created a workspace and then you also need to keep minor updates</p>
<p>[00:51:33] going on you might want to u you're doing a separate workspace your live workspace. So is it the workspace extras</p>
<p>[00:51:42] that allows you to pull in let's say you're making changes directly live in a campaign workspace can something like</p>
<p>[00:51:50] workspace extras pull in changes as long as they're not the same entities pull in uh changes live into the workspace so</p>
<p>[00:51:58] when you publish they don't get over yeah so that's a good question and um this I think maybe this is probably the last question that we have time for but</p>
<p>[00:52:07] it's an important one to answer uh so the question is um as you are working on a workspace and you have changes is in</p>
<p>[00:52:16] live how does that affect the content or can you pull in that those changes into the workspace so uh when I mentioned</p>
<p>[00:52:23] about git in terms of um how this is how workspace is related to git um think of this as more of a shallow copy so it's</p>
<p>[00:52:31] not that all these uh copies of every content are in this workspace it's just that whatever changes that you make are in this workspace so if you make changes</p>
<p>[00:52:40] to content on uh on live um that's uh the changes that you have in the workspace aren't necess if you're if</p>
<p>[00:52:48] you're not talking about the same content then the changes that you have in your workspace aren't going to affect that content. Thank you folks.</p>
<p>[00:52:56] Thank you.</p>
</div>]]></description>
      <pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Tag1</dc:creator>
      <guid>https://www.tag1.com/conference-sessions/drupalcon-2026-chicago-workspaces/</guid>
    </item>
    
    
    <item>
      <title>Preparing File Upload Secure Validator for Drupal 12 with AI</title>
      <link>https://www.tag1.com/blog/preparing-file-upload-secure-validator-drupal12/?utm_source=rss&amp;utm_medium=feed&amp;utm_campaign=planet_drupal</link>
      <description><![CDATA[<div style="--border-color: rgba(21, 120, 124, 0.5);"> 
<div class="border border-[var(--border-color)] relative my-12 " x-data="{
     boxId: $id('summary-box'),
     buttonId: $id('summary-box-button'),
     contentId: $id('summary-box-content'),
     isCollapsible: true,
     isOpen: false,
     toggle() {
         if (this.isCollapsible) {
             this.isOpen = !this.isOpen;
         }
     },
    }">
    <button type="button" x-bind:id="buttonId" class="w-full py-4 pl-4 flex items-center text-left" x-bind:class="{ 'pr-16': isCollapsible, 'pr-4': !isCollapsible }" x-on:click="toggle()" x-on:keyup.enter.prevent="toggle()" x-on:keyup.space.prevent="toggle()" x-bind:aria-expanded="isOpen" x-bind:aria-controls="contentId" x-bind:disabled="!isCollapsible">
        <h2 class="component text-xl font-text font-medium text-[var(--accent-color)]">Take Away</h2>
        <span x-show="isCollapsible" class="absolute top-1/2 right-4 -translate-y-1/2 flex items-center text-[var(--accent-color)] transition-transform duration-200" x-bind:class="{ 'rotate-180': isOpen }">
            <svg width="16" height="24" viewBox="0 0 16 24" xmlns="http://www.w3.org/2000/svg" class="rotate-90" aria-hidden="true">
                <path d="M0.753906 19.5016L4.49456 23.2456L15.7549 11.7451L4.49456 0.244572L0.753905 3.98965L8.41815 11.7461L0.753906 19.5016Z" fill="currentColor"></path>
            </svg>
        </span>
    </button>
    <div x-bind:id="contentId" role="region" x-bind:aria-labelledby="buttonId" x-show="isOpen || !isCollapsible" x-transition:enter="transition-opacity duration-500 ease-out motion-reduce:duration-0" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="pl-4 pr-20 md:pr-25 2xl:pr-32 pt-4 pb-4 text-lg font-text text-[var(--accent-color)] text-formatted">
<p>At Tag1, we believe in proving AI within our own work before recommending it to clients. This post is part of our <a href="https://www.tag1.com/insights/?tag=AI#selected">AI Applied content series</a>, where team members share real stories of how they're using Artificial Intelligence and the insights and lessons they learn along the way. Here, <a href="https://www.tag1.com/team/#stefanos-petrakis">Stefanos Petrakis</a>, maintainer of the File Upload Secure Validator module, shows how he used AI to modernize a small but widely used Drupal security module and prepare it for Drupal 12.</p>
    </div>
</div>
</div>
<h2>From &quot;I'll Get to It&quot; to Done: Modernizing File Upload Secure Validator</h2>
<p>I’ve been meaning to clean up the <a href="https://www.drupal.org/project/file_upload_secure_validator" target="_blank" rel="noopener noreferrer">File Upload Secure Validator</a> project and get it ready for Drupal 12 for a while now. This small, focused module has been around for nearly a decade. Despite its simplicity, it continues to serve more than 10,000 reported sites, and adoption has only accelerated with the introduction of <a href="https://www.drupal.org/project/ai" target="_blank" rel="noopener noreferrer">Drupal AI</a>. With the help of Cline and Claude, I finally did a full overhaul of the codebase: switching to Drupal 11-only support, expanding the automated test suite, and positioning the project for Drupal 12.</p>
<p></p><figure id="fig1" class="component my-10 grid gap-4">
<a href="https://www.tag1.com/img/blog/weekly-drupal-project-usage.png" target="_blank">
<img class="mx-auto  border-[var(--border-color)] border-2" src="https://www.tag1.com/img/blog/weekly-drupal-project-usage.png" alt="Graph showing Drupal usage over time and reflecting an increase since the release of Drupal AI." />
</a>
<figcaption class="italic text-[var(--text-color)]">Figure 1: Weekly File Upload Secure Validator usage report, that reflects an increase in usage post Drupal AI release.</figcaption>
</figure><p></p>
<h2>A Decade-Old Module Meets Drupal 12</h2>
<p>This was the kind of maintenance work I kept putting off, the same feeling I get when I need to sit down and do my taxes. I knew the project needed cleanup and modernization, but I wanted a little push and some company in doing the work. The missing motivation and sense of camaraderie were, in many ways, the biggest challenges.</p>
<p>On top of that, I had a clear vision for how I wanted to extend the test suite, and I knew it would be time-consuming. Time, or the lack of it, was a major factor, especially for this kind of detailed, behind-the-scenes work on an open source module.</p>
<h2>Turning a Wish List Into a Working Plan</h2>
<p>To move things forward, I turned to Cline and Claude to help plan the future of the module. I started by writing down a list of &quot;wishes&quot; for the project: the improvements I wanted to see in the code, tests, and overall quality.</p>
<p>Cline turned that list into a detailed execution plan. It also generated questions about the approach, which led us into a few iterations before we settled on the final course of action. That planning process gave structure to the work and made it much easier to tackle in focused sessions.</p>
<p>All of the changes happened in the project's repository on the <a href="https://git.drupalcode.org/project/file_upload_secure_validator/-/tree/2.2.x?ref_type=heads" target="_blank" rel="noopener noreferrer">2.2.x branch</a>, with the final result released as <a href="https://www.drupal.org/project/file_upload_secure_validator" target="_blank" rel="noopener noreferrer">version 2.2.1 on Drupal.org</a>.</p>
<h2>From Red CI Pipelines to Green Across the Board</h2>
<p>Before this overhaul, the project had accumulated a number of issues:</p>
<ul>
<li>Multiple GitLab CI failures</li>
<li>Drupal 11.3+ deprecation warnings</li>
<li>Unit test failures (including static method and <code>TranslatableMarkup</code> issues)</li>
<li>Limited test coverage</li>
<li>383+ PHPCS violations</li>
<li>CSpell errors</li>
<li>PHPStan attribute errors</li>
</ul>
<p>After the overhaul, the picture looks very different:</p>
<ul>
<li>All GitLab CI tests passing</li>
<li>Zero deprecation warnings</li>
<li>23 tests with 164 assertions</li>
<li>0 PHPCS errors and 0 warnings</li>
<li>0 CSpell errors</li>
<li>0 PHPStan errors</li>
<li>100% CI quality checks passing</li>
</ul>
<p>This overhaul gave me the &quot;manpower&quot; and momentum I was missing to push the project forward. Just as importantly, it gave me confidence that I can continue supporting this module in the future.</p>
<h2>AI-Amplified Maintenance for Critical Dependencies</h2>
<p>Maintaining and supporting open source libraries can often become demanding because of limited time and resources. In client projects, dependencies on under-maintained open source projects can increase the effort required to maintain or upgrade the client's own platform.</p>
<p>Partners like  Cline and Claude can change the game in an advantageous way. Such a change can help teams keep critical open source dependencies up to date, improve quality, and reduce risk without requiring a huge amount of extra human capacity.</p>
<div class="highlight-box">
<p>This post is part of Tag1’s <a href="https://www.tag1.com/insights/?tag=AI&type=blogPosts#selected">AI Applied content series</a>, where we share how we're using AI inside our own work before bringing it to clients. Our goal is to be transparent about what works, what doesn’t, and what we are still figuring out, so that together, we can build a more practical, responsible path for AI adoption.</p>
</div>
<p><em>Bring practical, proven AI adoption strategies to your organization, let's start a conversation! <a href="https://www.tag1.com/contact/">We'd love to hear from you.</a></em></p>
]]></description>
      <pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate>
      
      
      
        
      
      <dc:creator>Stefanos Petrakis</dc:creator>
      <guid>https://www.tag1.com/blog/preparing-file-upload-secure-validator-drupal12/</guid>
    </item>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
  </channel>
</rss>
