# AGENTS.md - AI Coding Assistant Guide for FlowDrop

This document provides AI coding assistants with essential context for contributing to the FlowDrop project effectively.

## Project Overview

**FlowDrop** is a Drupal-based visual workflow editor that enables site builders to create, configure, and execute automated workflows through a drag-and-drop interface.

### Technology Stack

| Component         | Technology                                  |
| ----------------- | ------------------------------------------- |
| Backend           | PHP 8.2+, Drupal 11                         |
| Frontend          | Svelte 5 + XYFlow (@flowdrop/flowdrop)       |
| Package Manager   | Composer                                    |
| Local Environment | DDEV (with drupal-contrib addon)            |
| Code Quality      | PHPStan (level 5), PHPCS (Drupal standards) |
| Testing           | PHPUnit                                     |

### Local Development

The project uses **DDEV** for local development. The site is accessible at:

- **URL**: `https://flowdrop.ddev.site`
- **Launch**: `ddev launch`

All development tools (PHPStan, PHPCS, PHPCBF, Drush) should be run through DDEV using the "ddev drupal-contrib"'s addon commands.

Eg. `ddev phpstan`, `ddev phpcbf`, `ddev phpcs`.

### Frontend Library (FlowDrop UI)

The visual workflow editor is powered by the **FlowDrop UI** JavaScript library, maintained by the same team:

- **NPM Package**: [@flowdrop/flowdrop](https://www.npmjs.com/package/@flowdrop/flowdrop)
- **Source Repository**: [flowdrop-io/flowdrop](https://github.com/flowdrop-io/flowdrop)
- **API Specification**: [openapi.yaml](https://github.com/flowdrop-io/flowdrop/blob/1.x/api/openapi.yaml)

The library is built with Svelte 5 + XYFlow and provides the drag-and-drop workflow editing interface. Since this library is maintained by the same team, new features can be added to support the Drupal module's requirements.

### Core Concepts

- **Workflow**: A directed graph of steps that transforms inputs into outputs within a defined context.
- **Workflow Node**: A discrete, reusable step with typed inputs, typed outputs, and configuration.
- **Node Processor**: The backend component that executes a node; should be idempotent and deterministic.
- **Pipeline**: A queued workflow execution instance.
- **Job**: An individual node execution within a pipeline.
- **Trigger**: An event-driven mechanism that initiates workflow execution (e.g., entity events).
- **Data Edge**: An edge that passes data from one node's output port to another node's input port.
- **Trigger Edge**: An edge that controls execution flow between nodes.

---

## Architecture Summary

```
flowdrop/
├── src/                              # Core PHP classes
│   ├── Attribute/                    # Plugin discovery attributes
│   ├── Controller/                   # Drupal controllers
│   ├── DTO/                          # Data Transfer Objects
│   ├── Form/                         # Drupal forms
│   ├── Hook/                         # Drupal hook implementations
│   ├── Plugin/FlowDropNodeProcessor/ # Base node processor classes
│   ├── Routing/                      # Route subscribers
│   └── Service/                      # Core services
├── modules/                          # Drupal sub-modules
│   ├── flowdrop_workflow/            # Workflow entity management
│   ├── flowdrop_runtime/             # Workflow execution engine
│   ├── flowdrop_stategraph/          # State graph orchestration
│   ├── flowdrop_pipeline/            # Pipeline management
│   ├── flowdrop_job/                 # Job execution system
│   ├── flowdrop_node_type/           # Node type definitions
│   ├── flowdrop_node_category/       # Node categorization
│   ├── flowdrop_node_processor/      # Built-in node processors
│   ├── flowdrop_trigger/             # Event-driven triggers
│   ├── flowdrop_ui_components/       # Reusable UI components
│   └── flowdrop_playground/          # Development/testing tools
├── config/                           # Drupal configuration
├── js/                               # JavaScript assets
└── css/                              # CSS assets
```

### Plugin System

FlowDrop uses PHP 8 attributes for automatic plugin discovery:

```php
#[FlowDropNodeProcessor(
  id: "my_processor",
  label: new TranslatableMarkup("My Processor"),
  description: "Description of the processor",
  version: "1.0.0"
)]
class MyProcessor extends AbstractFlowDropNodeProcessor { }
```

Plugins are discovered in `Plugin/FlowDropNodeProcessor` directories across all modules.

#### Plugin ID Namespacing

All FlowDropNodeProcessor plugin IDs are **automatically namespaced** with their provider module name to prevent ID collisions when multiple modules define plugins with the same base ID.

**Format:** `{provider_module}:{original_id}`

For example, a plugin defined with `id: "chat_model"` in module `flowdrop_ai_provider` becomes `flowdrop_ai_provider:chat_model`.

| Attribute ID   | Provider Module           | Namespaced ID                              |
| -------------- | ------------------------- | ------------------------------------------ |
| `text_input`   | `flowdrop_node_processor` | `flowdrop_node_processor:text_input`       |
| `trigger`      | `flowdrop_trigger`        | `flowdrop_trigger:trigger`                 |
| `confirmation` | `flowdrop_interrupt`      | `flowdrop_interrupt:confirmation`          |
| `chat_model`   | `flowdrop_ai_provider`    | `flowdrop_ai_provider:chat_model`          |

**When referencing plugins:**
- In `executor_plugin` config: Use the **namespaced ID** (e.g., `flowdrop_node_processor:text_input`)
- In plugin attribute `id`: Use the **original ID** (e.g., `text_input`) - namespacing is automatic
- When calling `createInstance()`: Use the **namespaced ID**

**Helper methods on `FlowDropNodeProcessorPluginManager`:**
- `isNamespaced($id)` - Check if an ID contains a namespace separator
- `buildNamespacedId($provider, $originalId)` - Build a namespaced ID
- `getProviderFromNamespacedId($id)` - Extract provider from namespaced ID
- `getOriginalIdFromNamespacedId($id)` - Extract original ID from namespaced ID

The `original_id` is preserved in the plugin definition for reference.

### Key Services

| Service                                        | Responsibility                                     |
| ---------------------------------------------- | -------------------------------------------------- |
| `flowdrop.node_processor_plugin_manager`       | Manages node processor discovery and instantiation |
| `flowdrop_runtime.node_runtime`                | Executes individual nodes                          |
| `flowdrop_runtime.synchronous_orchestrator`    | Runs workflows synchronously                       |
| `flowdrop_runtime.asynchronous_orchestrator`   | Runs workflows via queues                          |
| `flowdrop_stategraph.state_graph_orchestrator` | Manages state graph execution                      |
| `flowdrop_pipeline.job_generation`             | Generates jobs from workflows                      |
| `flowdrop_trigger.trigger_manager`             | Processes trigger events                           |

---

## Coding Standards and Conventions

### Hard Rules (Always Follow)

- ALWAYS include `declare(strict_types=1)` at the top of every PHP file.
- ALWAYS use double quotes (`"`) for strings — never single quotes.
- ALWAYS use the **namespaced ID** (`{module}:{id}`) when calling `createInstance()` or setting `executor_plugin`.
- ALWAYS clear cache (`ddev drush cr`) after changing plugin attributes, service definitions, config schemas, or hooks.
- NEVER log routine operations in node processors — logging is handled by the orchestrator via `ExecutionLogger`.
- NEVER expose `__` prefixed internal parameters (e.g., `__iterator__`, `__state__`) to users in node type configuration.
- NEVER create helper methods that are only referenced once — inline the logic instead.
- NEVER use `assert()` — use runtime-safe `if` checks with exceptions instead.
- NEVER use the `any` type, non-null assertion operator (`!`), or cast to unknown.

### After Making Changes

Run these steps automatically after code changes — do not ask for permission:

1. `ddev phpcbf` — auto-fix coding standard violations
2. `ddev phpcs` — verify no remaining violations
3. `ddev phpstan` — run static analysis (level 5)
4. `ddev drush cr` — clear cache (if plugins, services, config, or hooks changed)
5. `ddev phpunit modules/{changed_module}/tests/` — run tests for the affected module

### PHP Requirements

1. **Strict Types**: Always include at the top of every PHP file:
   ```php
   <?php

   declare(strict_types=1);
   ```

2. **Drupal Coding Standards**: Code must pass PHPCS with Drupal ruleset.

3. **PHPStan Level 5**: Code must pass static analysis at level 5.

4. **String Quotes**: Use double quotes (`"`) for strings:
   ```php
   // Correct
   $message = "Hello, world!";
   $template = "Value: {$value}";

   // Incorrect
   $message = 'Hello, world!';
   ```

5. **Type Safety**:
   - Do NOT use the `any` type
   - Do NOT use non-null assertion operator (`!`)
   - Do NOT cast to unknown (`as unknown as T`)
   - Use strict type hints for all parameters and return types

6. **Constructor Property Promotion**: Use PHP 8 constructor property promotion:
   ```php
   public function __construct(
     array $configuration,
     $plugin_id,
     $plugin_definition,
     private readonly ClientFactory $clientFactory,
   ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
   }
   ```

7. **Documentation**: Include clear inline comments and JSDoc/PHPDoc headers:
   ```php
   /**
    * Executes an HTTP request.
    *
    * @param \Drupal\flowdrop\DTO\ParameterBagInterface $params
    *   The parameter bag containing request configuration.
    *
    * @return array<string, mixed>
    *   The response data including status code, headers, and body.
    *
    * @throws \Exception
    *   When the request fails or URL is invalid.
    */
   public function process(ParameterBagInterface $params): array {
   ```

8. **Error Handling**: Implement error checking and type validation:
   ```php
   if (empty($url)) {
     throw new \Exception("No URL provided for HTTP request");
   }

   if (!filter_var($url, FILTER_VALIDATE_URL)) {
     throw new \Exception("Invalid URL provided: " . $url);
   }
   ```

---

## Common Patterns with Code Examples

### Creating a Node Processor

> Full reference with all methods: [docs/development/flowdrop-node-processor.md](docs/development/flowdrop-node-processor.md)

```php
<?php

declare(strict_types=1);

namespace Drupal\my_module\Plugin\FlowDropNodeProcessor;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;

/**
 * Processor for custom data transformation.
 */
#[FlowDropNodeProcessor(
  id: "my_custom_processor",
  label: new TranslatableMarkup("My Custom Processor"),
  description: "Transforms data according to custom rules",
  version: "1.0.0"
)]
class MyCustomProcessor extends AbstractFlowDropNodeProcessor {

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $inputData = $params->get("input_data", "");
    $format = $params->getString("format", "json");

    return [
      "result" => $this->doTransform($inputData, $format),
      "processed_at" => time(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "input_data" => [
          "type" => "string",
          "title" => "Input Data",
          "description" => "The data to transform",
          "default" => "",
        ],
        "format" => [
          "type" => "string",
          "title" => "Output Format",
          "description" => "The desired output format",
          "default" => "json",
          "enum" => ["json", "xml", "csv"],
        ],
      ],
    ];
  }

}
```

Override `create()` to inject services, `validateParams()` for input validation, and `getOutputSchema()` to describe outputs. See the full reference linked above.

### Parameter Schema Properties

Use standard JSON Schema properties:

| Property                  | Type    | Description                                                                     |
| ------------------------- | ------- | ------------------------------------------------------------------------------- |
| `type`                    | string  | Data type: `string`, `number`, `integer`, `boolean`, `array`, `object`, `mixed` |
| `description`             | string  | Human-readable description                                                      |
| `default`                 | mixed   | Default value                                                                   |
| `enum`                    | array   | Allowed values                                                                  |
| `minimum` / `maximum`     | number  | Numeric constraints                                                             |
| `minLength` / `maxLength` | integer | String length constraints                                                       |
| `pattern`                 | string  | Regex pattern for strings                                                       |
| `format`                  | string  | Format hint: `email`, `uri`, `date`, `multiline`, `json`                        |
| `readOnly`                | boolean | Value cannot be edited but can be exposed (e.g., for `branches`)                |

### ParameterBag Methods

```php
// Get any parameter value
$value = $params->get("key", $default);

// Get typed values
$text = $params->getString("text", "");
$count = $params->getInt("count", 0);
$rate = $params->getFloat("rate", 0.0);
$enabled = $params->getBool("enabled", false);
$items = $params->getArray("items", []);

// Check existence
if ($params->has("optional_key")) { }

// Get all parameters
$all = $params->all();
```

### Creating a DTO

```php
<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO;

/**
 * Data transfer object for workflow execution results.
 */
final class ExecutionResult {

  /**
   * Constructs an ExecutionResult object.
   *
   * @param string $status
   *   The execution status.
   * @param array<string, mixed> $data
   *   The result data.
   * @param string|null $error
   *   Optional error message.
   */
  public function __construct(
    public readonly string $status,
    public readonly array $data,
    public readonly ?string $error = null,
  ) {}

  /**
   * Creates an instance from an array.
   *
   * @param array<string, mixed> $data
   *   The source array.
   *
   * @return self
   *   A new instance.
   */
  public static function fromArray(array $data): self {
    return new self(
      status: $data["status"] ?? "unknown",
      data: $data["data"] ?? [],
      error: $data["error"] ?? null
    );
  }

  /**
   * Converts the object to an array.
   *
   * @return array<string, mixed>
   *   The array representation.
   */
  public function toArray(): array {
    return [
      "status" => $this->status,
      "data" => $this->data,
      "error" => $this->error,
    ];
  }

}
```

### Service Definition

In `*.services.yml`:

```yaml
services:
  my_module.my_service:
    class: Drupal\my_module\Service\MyService
    arguments:
      - "@entity_type.manager"
      - "@logger.factory"
```

---

## Key Files Reference

### Core Module

| File                                                                  | Purpose                            |
| --------------------------------------------------------------------- | ---------------------------------- |
| `src/Plugin/FlowDropNodeProcessor/AbstractFlowDropNodeProcessor.php`  | Base class for all node processors |
| `src/Plugin/FlowDropNodeProcessor/FlowDropNodeProcessorInterface.php` | Interface for node processors      |
| `src/Attribute/FlowDropNodeProcessor.php`                             | Plugin attribute definition        |
| `src/DTO/ParameterBag.php`                                            | Parameter container implementation |
| `src/DTO/ParameterBagInterface.php`                                   | Parameter container interface      |
| `src/DTO/ValidationResult.php`                                        | Validation result container        |
| `src/Service/FlowDropNodeProcessorPluginManager.php`                  | Plugin manager service             |

### Runtime Module

| File                                                                             | Purpose                  |
| -------------------------------------------------------------------------------- | ------------------------ |
| `modules/flowdrop_runtime/src/Service/Runtime/NodeRuntimeService.php`            | Node execution service   |
| `modules/flowdrop_runtime/src/Service/Orchestrator/SynchronousOrchestrator.php`  | Sync workflow execution  |
| `modules/flowdrop_runtime/src/Service/Orchestrator/AsynchronousOrchestrator.php` | Async workflow execution |
| `modules/flowdrop_runtime/src/DTO/Runtime/NodeExecutionContext.php`              | Node execution context   |
| `modules/flowdrop_runtime/src/DTO/Runtime/NodeExecutionResult.php`               | Node execution result    |

### StateGraph Module

| File                                                                 | Purpose               |
| -------------------------------------------------------------------- | --------------------- |
| `modules/flowdrop_stategraph/src/Service/StateGraphOrchestrator.php` | State graph execution |
| `modules/flowdrop_stategraph/src/DTO/GraphState.php`                 | Graph state container |
| `modules/flowdrop_stategraph/src/DTO/IteratorState.php`              | Loop iteration state  |

### UI Components Module

| Directory                                      | Purpose                       |
| ---------------------------------------------- | ----------------------------- |
| `modules/flowdrop_ui_components/components/`   | Reusable Drupal components    |
| `modules/flowdrop_ui_components/css/`          | Design tokens and base styles |
| `modules/flowdrop_ui_components/lib/flowdrop/` | Svelte editor build           |

---

## Testing Guidelines

### Test Structure

```
modules/{module_name}/tests/
├── Unit/                    # Unit tests (isolated, no Drupal bootstrap)
│   └── Service/
│       └── MyServiceTest.php
├── Kernel/                  # Kernel tests (partial Drupal bootstrap)
│   └── MyKernelTest.php
└── Integration/             # Integration tests (full functionality)
    └── MyIntegrationTest.php
```

### Unit Test Example

```php
<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\DTO\ParameterBag;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the MyService class.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\MyService
 * @group flowdrop_runtime
 */
class MyServiceTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The service under test.
   *
   * @var \Drupal\flowdrop_runtime\Service\MyService
   */
  protected $service;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    // Setup code here...
  }

  /**
   * Tests the process method with valid input.
   *
   * @covers ::process
   */
  public function testProcessWithValidInput(): void {
    $params = new ParameterBag(["key" => "value"]);
    $result = $this->service->process($params);

    $this->assertArrayHasKey("result", $result);
    $this->assertEquals("expected", $result["result"]);
  }

}
```

### Kernel Test Example

```php
<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\flowdrop_workflow\Entity\FlowDropWorkflow;

/**
 * Tests the FlowDropWorkflow entity.
 *
 * @group flowdrop_workflow
 */
class FlowDropWorkflowKernelTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    "system",
    "user",
    "flowdrop",
    "flowdrop_workflow",
    "flowdrop_node_type",
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installEntitySchema("user");
    $this->installEntitySchema("flowdrop_workflow");
  }

  /**
   * Tests workflow entity creation.
   */
  public function testWorkflowCreation(): void {
    $workflow = FlowDropWorkflow::create([
      "id" => "test_workflow",
      "label" => "Test Workflow",
    ]);
    $workflow->save();

    $this->assertNotNull($workflow->id());
  }

}
```

### Running Tests

```bash
# Run all FlowDrop tests (via DDEV)
ddev phpunit modules/flowdrop*/tests/

# Run specific module tests
ddev phpunit modules/flowdrop_runtime/tests/

# Run specific test class
ddev phpunit modules/flowdrop_runtime/tests/Unit/Service/MyServiceTest.php

# Run with coverage
ddev phpunit --coverage-html coverage/ modules/flowdrop*/tests/
```

---

## Common Tasks Checklist

### Adding a New Node Processor

1. Create PHP file in `modules/{your_module}/src/Plugin/FlowDropNodeProcessor/`
2. Extend `AbstractFlowDropNodeProcessor`
3. Add `#[FlowDropNodeProcessor]` attribute with a unique base ID (will be auto-namespaced as `{module}:{id}`)
4. Implement `process()` method
5. Implement `getParameterSchema()` method
6. Implement `getOutputSchema()` method (optional, has default)
7. Add service dependencies via `create()` if needed
8. Clear cache: `ddev drush cr`
9. Create node type config entity with `executor_plugin: '{module}:{id}'` (optional, for UI customization)
10. Write tests

### Adding a New Service

1. Create service class in `src/Service/` or `modules/{module}/src/Service/`
2. Define interface if needed
3. Add to `*.services.yml`
4. Inject dependencies via constructor
5. Clear cache: `ddev drush cr`
6. Write tests

### Debugging Workflows

```bash
# Clear cache
ddev drush cr

# Check plugin discovery
ddev drush ev "print_r(\Drupal::service('flowdrop.node_processor_plugin_manager')->getDefinitions());"

# Watch logs
ddev drush watchdog:tail --filter=flowdrop

# Test a trigger
ddev drush flowdrop:trigger:test entity.insert node 1

# List active triggers
ddev drush flowdrop:trigger:list
```

---

## Important Warnings

### Internal Parameters

Parameters prefixed with `__` (e.g., `__iterator__`, `__state__`, `__messages__`) are internal system parameters with special handling:

- They bypass the `connectable` check (always accept runtime inputs)
- They bypass the `configurable` check (users cannot set them)
- They are injected by the orchestrator, not configured in the UI

**NEVER expose internal parameters to users in node type configuration.**

See `docs/development/internal-parameters.md` for complete documentation.

### Loopback Edges

Loopback edges (used for loop iteration) are excluded from:
- Dependency checking
- Cycle detection

This is intentional to enable loop constructs.

### Cache Management

After making changes to:
- Plugin attributes
- Service definitions
- Configuration schemas
- Hook implementations

Always clear the cache:

```bash
ddev drush cr
```

### Logging Policy

Node processors MUST NOT log routine operations. Logging is handled by the orchestration layer via `ExecutionLogger`.

Processors MUST ONLY log exceptional events:
- External service retries/failures
- Fallback usage
- Rate limits
- Unexpected data shapes

---

## Common Mistakes

Avoid these anti-patterns observed in the project:

- **Wrong plugin ID**: Using `createInstance("text_input")` instead of `createInstance("flowdrop_node_processor:text_input")` — always use the namespaced ID.
- **Missing cache clear**: Forgetting `ddev drush cr` after changing plugin attributes, services, or config schemas — the change won't take effect.
- **Logging in processors**: Adding `$this->logger->info()` in a node processor's `process()` method — this is handled by the orchestrator's `ExecutionLogger`.
- **Using `assert()`**: Using `assert($value !== null)` — use `if ($value === null) { throw ... }` instead (assert can be disabled at runtime).
- **Single quotes**: Using `'string'` instead of `"string"` — this project uses double quotes consistently.
- **One-off helpers**: Creating a `private function formatValue()` called from exactly one place — inline the logic instead.
- **Exposing internal params**: Adding `__iterator__` or `__state__` to a node type's parameter schema — these are system-injected and must not be user-configurable.

---

## Documentation References

- [DEVELOPMENT.md](DEVELOPMENT.md) - Development setup guide
- [docs/development/flowdrop-node-processor.md](docs/development/flowdrop-node-processor.md) - Node processor development
- [docs/development/gateway-nodes.md](docs/development/gateway-nodes.md) - Gateway nodes, `branches`, and `active_branches`
- [docs/development/internal-parameters.md](docs/development/internal-parameters.md) - Internal parameter system
- [docs/development/execution-dependency-rules.md](docs/development/execution-dependency-rules.md) - Execution rules
- [docs/meta/glossary.md](docs/meta/glossary.md) - Project terminology
- [modules/flowdrop_trigger/TESTING.md](modules/flowdrop_trigger/TESTING.md) - Trigger testing guide

---

## Quick Reference Card

```
# DDEV Commands
ddev launch                           # Open site in browser (https://flowdrop.ddev.site)
ddev drush cr                         # Clear cache
ddev drush watchdog:tail              # Watch logs

# Code Quality (via DDEV drupal-contrib addon)
ddev phpcs                            # Check coding standards
ddev phpcbf                           # Auto-fix coding standard violations
ddev phpstan                          # Run static analysis (level 5)
ddev phpunit path/to/tests/           # Run tests

# Key Namespaces
Drupal\flowdrop\Plugin\FlowDropNodeProcessor\   # Node processors
Drupal\flowdrop\DTO\                            # Data transfer objects
Drupal\flowdrop_runtime\Service\                # Runtime services
Drupal\flowdrop_stategraph\Service\             # State graph services

# Key Interfaces
FlowDropNodeProcessorInterface     # Node processor contract
ParameterBagInterface              # Parameter container contract
OrchestratorInterface              # Workflow orchestrator contract
```
