---
name: diagrams-js/diagrams-js-plugin-system
description: >-
  Extend diagrams-js with custom import/export formats, metadata providers, and hooks.
  Create plugins for Docker Compose, Terraform, cloud provider metadata, and more.
type: feature
library: diagrams-js
---

# diagrams-js Plugin System

Extend diagrams-js with custom importers, exporters, metadata providers, and lifecycle hooks.

## Quick Start

### Creating a Simple Plugin

```typescript
// Create a custom exporter plugin
// Note: Use context.lib to access diagrams-js exports instead of importing
const myExporter = () => ({
  name: "my-exporter",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: {
    node: true,
    browser: true,
    deno: true,
    bun: true,
  },
  capabilities: [
    {
      type: "exporter",
      name: "my-format",
      extension: ".my",
      mimeType: "text/plain",
      export: async (diagram, context) => {
        // Access library exports via context.lib
        const { Node, Edge } = context.lib;
        return `Diagram: ${diagram.name}`;
      },
    },
  ],
});

// Use the plugin (import only needed for Diagram creation)
import { Diagram } from "diagrams-js";

const diagram = Diagram("My Architecture");

// Register plugins explicitly
await diagram.registerPlugins([myExporter]);

// Export to custom format
const result = await diagram.export("my-format");
console.log(result); // "Diagram: My Architecture"
```

### Built-in Plugins

Built-in plugins (JSON and SVG) are automatically registered when you call plugin-related methods:

```typescript
const diagram = Diagram("My Architecture");

// Built-in JSON plugin auto-registers on first use
const json = await diagram.export("json"); // Works without explicit registration
const json2 = diagram.toJSON(); // Also works

// Built-in SVG plugin auto-registers on first use
const svg = await diagram.export("svg"); // Works without explicit registration
const restored = await Diagram.fromSVG(svg); // Re-import from SVG
```

## Plugin Capabilities

### Importer

Import diagrams from external formats:

```typescript
const dockerComposePlugin = () => ({
  name: "docker-compose",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "importer",
      name: "docker-compose",
      extensions: [".yml", ".yaml"],
      mimeTypes: ["text/yaml"],
      canImport: async (source) => {
        return source.includes("services:");
      },
      import: async (source, diagram, context) => {
        // Use context.loadYaml() instead of bundling yaml parser
        const yaml = await context.loadYaml();
        const compose = yaml.load(source);

        // Access Node from context.lib instead of importing
        const { Node } = context.lib;

        // Create nodes for each service
        for (const [name, service] of Object.entries(compose.services)) {
          const node = diagram.add(Node(name));
          node.metadata = { image: service.image };
        }
      },
    },
  ],
});

// Usage
const diagram = Diagram("My App");
await diagram.registerPlugins([dockerComposePlugin]);

// Import single source
await diagram.import(yamlContent, "docker-compose");

// Import multiple sources - each in its own cluster
await diagram.import([compose1, compose2, compose3], "docker-compose");
```

### Exporter

Export diagrams to external formats:

```typescript
const terraformPlugin = () => ({
  name: "terraform",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "exporter",
      name: "terraform",
      extension: ".tf",
      mimeType: "text/plain",
      export: async (diagram, context) => {
        let tf = `# Terraform generated from ${diagram.name}\n\n`;

        for (const node of diagram.toJSON().nodes) {
          tf += `resource "aws_instance" "${node.id}" {\n`;
          tf += `  # Configuration...\n`;
          tf += `}\n\n`;
        }

        return tf;
      },
    },
  ],
});

// Usage
const terraform = await diagram.export("terraform");
```

### Metadata Provider

Attach cloud provider metadata to nodes:

```typescript
const awsMetadataPlugin = () => ({
  name: "aws-metadata",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "metadata",
      provider: "aws",
      nodeTypes: ["EC2", "RDS", "Lambda"],
      getMetadata: async (nodeType, config, context) => {
        // Fetch pricing/specs from AWS API
        return {
          provider: "aws",
          specifications: {
            cpu: 4,
            memory: "16GB",
          },
          pricing: {
            hourly: 0.192,
            monthly: 140.16,
          },
        };
      },
    },
  ],
});

// Usage
const diagram = Diagram("My App");
await diagram.registerPlugins([awsMetadataPlugin]);

const ec2 = diagram.add(EC2("Web Server"));

// Attach metadata to all EC2 nodes
await diagram.attachMetadata("aws", "EC2");

// Access metadata
console.log(ec2.metadata.pricing.monthly); // 140.16
```

### Hooks

Execute code at lifecycle events:

```typescript
const loggingPlugin = () => ({
  name: "logging",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "hook",
      hooks: [
        {
          event: "before:export",
          handler: async (data, context) => {
            console.log(`Exporting to ${data.format}...`);
            return data;
          },
        },
        {
          event: "after:export",
          handler: async (data, context) => {
            console.log("Export complete!");
            return data;
          },
        },
      ],
    },
  ],
});
```

#### SVG Post-Processing with `after:layout`

The `after:layout` hook fires after Graphviz renders the SVG but before format conversion to PNG/JPG. This is the ideal place for SVG string manipulation that should also affect raster output.

```typescript
const svgPostProcessor = () => ({
  name: "svg-post-processor",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "hook",
      hooks: [
        {
          event: "after:layout",
          handler: async ({ svg, diagram, format }) => {
            // Modify SVG string - affects both SVG and PNG/JPG output
            const modified = svg.replace(/stroke="#7b8894"/g, 'stroke="red"');
            return { svg: modified, diagram, format };
          },
        },
      ],
    },
  ],
});
```

#### Available Hook Events

- `before:import` / `after:import` - Import operations
- `before:export` / `after:export` - Export operations
- `before:render` / `after:render` - Rendering
- `after:layout` - After Graphviz produces SVG, before format conversion (PNG/JPG)
- `before:serialize` / `after:deserialize` - JSON serialization
- `node:create` - Node creation
- `edge:create` - Edge creation
- `cluster:create` - Cluster creation
- `metadata:attach` - Metadata attachment

## Plugin Dependencies

Plugins can declare dependencies on other plugins:

```typescript
const advancedPlugin = () => ({
  name: "advanced",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  dependencies: ["base-plugin"], // Depends on base-plugin
  capabilities: [
    {
      type: "exporter",
      name: "advanced-format",
      extension: ".adv",
      mimeType: "text/plain",
      export: async (diagram, context) => {
        // Can access base-plugin's capabilities
        const baseExporter = context.getExporter("base-format");
        const base = await baseExporter.export(diagram, context);
        return `Advanced: ${base}`;
      },
    },
  ],
});

// Register in correct order (dependencies first)
const diagram = Diagram("Test");
await diagram.registerPlugins([basePlugin, advancedPlugin]);
```

## Plugin Registry

Access the plugin registry to query capabilities:

```typescript
const diagram = Diagram("Test");
await diagram.registerPlugins([myPlugin]);

// List all capabilities
const caps = diagram.registry.listCapabilities();
console.log(caps.importers); // ["docker-compose", ...]
console.log(caps.exporters); // ["terraform", "json", ...]

// Get specific plugin
const importer = diagram.registry.getImporter("docker-compose");
const exporter = diagram.registry.getExporter("terraform");
const provider = diagram.registry.getMetadataProvider("aws");

// List all registered plugins
const plugins = diagram.registry.listPlugins();
```

## Best Practices

### 1. Use Factory Functions

Always create plugins as factory functions:

```typescript
// ✅ Good
const myPlugin = (config) => ({
  name: "my-plugin",
  // ...
});

// ❌ Avoid
const myPlugin = {
  name: "my-plugin",
  // ...
};
```

### 2. Declare Runtime Support

Always declare which runtimes your plugin supports:

```typescript
runtimeSupport: {
  node: true,      // Works in Node.js
  browser: false,  // Doesn't work in browsers (uses fs)
  deno: false,     // Not tested in Deno
  bun: false,      // Not tested in Bun
}
```

### 3. Use Context-Provided Utilities

For YAML parsing and other common needs, use the utilities provided in the plugin context instead of bundling your own dependencies:

```typescript
// ✅ Good: Use context.loadYaml() - no bundling needed
import: async (source, diagram, context) => {
  const yaml = await context.loadYaml();
  const parsed = yaml.load(source);
  // ...
}

// ❌ Avoid: Bundling your own YAML parser
import: async (source, diagram, context) => {
  const yaml = await import("yaml"); // Adds bundle size
  const parsed = yaml.parse(source);
  // ...
}
```

This approach:

- Reduces plugin bundle size
- Ensures consistent parsing across plugins
- Works across all runtimes automatically

### 4. Validate Configuration

Use `requiredConfig` and `initialize`:

```typescript
const apiPlugin = () => ({
  name: "api-plugin",
  version: "1.0.0",
  apiVersion: "1.0",
  requiredConfig: ["apiKey", "region"],
  async initialize(config, context) {
    if (!config.apiKey) {
      throw new Error("apiKey is required");
    }
    // Set up API client
  },
  // ...
});

// Usage with config
const diagram = Diagram("Test");
await diagram.registerPlugins([[apiPlugin, { apiKey: "xxx", region: "us-west-2" }]]);
```

## Common Patterns

### Importer with YAML

```typescript
const myImporter = () => ({
  name: "my-format",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "importer",
      name: "my-format",
      extensions: [".yml", ".yaml"],
      canImport: async (source) => source.includes("services:"),
      import: async (source, diagram, context) => {
        const yaml = await context.loadYaml();
        const data = yaml.load(source);
        const { Node } = context.lib;
        for (const [name, service] of Object.entries(data.services)) {
          diagram.add(Node(name)).metadata = { image: service.image };
        }
      },
    },
  ],
});
```

### Metadata Provider

```typescript
const awsMetadataPlugin = () => ({
  name: "aws-metadata",
  version: "1.0.0",
  apiVersion: "1.0",
  runtimeSupport: { node: true, browser: true, deno: true, bun: true },
  capabilities: [
    {
      type: "metadata",
      provider: "aws",
      nodeTypes: ["EC2", "RDS", "Lambda"],
      getMetadata: async (nodeType, config, context) => ({
        provider: "aws",
        pricing: { hourly: 0.192, monthly: 140.16 },
        specifications: { cpu: 4, memory: "16GB" },
      }),
    },
  ],
});
```

## Further Reading
