Motia Icon
Development Guide

Plugins

Learn how to create and use plugins to extend Motia's functionality

Plugins

Plugins are a powerful way to extend Motia's functionality by adding custom features to the workbench, and integrating with external services.

What are Plugins?

Plugins in Motia allow you to:

Custom Workbench Tabs

Add custom tabs to the workbench interface

Specialized Visualizations

Create rich tooling tailored to your workflows

Service Integrations

Integrate with external services and APIs

Core Extensions

Extend Motia's core functionality with new capabilities

Reusable Modules

Share reusable functionality across projects

Motia comes with several official plugins like plugin-logs, plugin-endpoint, plugin-observability, and plugin-states that demonstrate the power and flexibility of the plugin system.

Plugin Architecture

Core Types

Plugins are built using three main TypeScript types:

MotiaPlugin

The main plugin configuration returned by your plugin function:

type MotiaPlugin = {
  workbench: WorkbenchPlugin[]  // Array of workbench tab configurations
  dirname?: string              // Optional plugin directory
  steps?: string[]              // Optional custom steps
}

WorkbenchPlugin

Configuration for a workbench tab:

type WorkbenchPlugin = {
  packageName: string           // Package registry name (e.g., '@motiadev/plugin-example')
  componentName?: string        // React component name to render
  label?: string                // Tab label text
  labelIcon?: string            // Icon name from lucide-react
  position?: 'bottom' | 'top'   // Tab position in workbench
  cssImports?: string[]         // CSS files to import
  props?: Record<string, any>   // Props passed to component
}

MotiaPluginContext

Context object provided to your plugin with access to Motia's internal APIs:

type MotiaPluginContext = {
  printer: Printer              // Logging utilities
  state: StateAdapter           // State management
  lockedData: LockedData        // Thread-safe data access
  tracerFactory: TracerFactory  // Tracing functionality
  registerApi: (...)            // Register custom API endpoints
}

Creating a Plugin

Quick Start with CLI

The fastest way to create a new plugin is using the Motia CLI:

pnpm dlx motia create --plugin my-plugin

After creation, set up and verify the build pipeline:

cd my-plugin
pnpm install
pnpm run build

What the CLI template includes

  • TypeScript and React configuration
  • Vite build setup
  • Example workbench UI component
  • Required dependencies pre-installed
  • Ready-to-build project structure

Manual Setup

If you prefer to set up manually or want to understand the structure, follow these steps:

Create a new directory for your plugin with the following structure:

index.ts
plugin.ts
styles.css
package.json
tsconfig.json
vite.config.ts
postcss.config.js
README.md

Create a package.json with proper exports for both the main entry and plugin definition:

{
  "name": "@motiadev/plugin-example",
  "version": "0.8.2-beta.139",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./plugin": {
      "types": "./dist/plugin.d.ts",
      "import": "./dist/plugin.js",
      "require": "./dist/plugin.cjs"
    },
    "./styles.css": "./dist/styles.css"
  },
  "peerDependencies": {
    "@motiadev/ui": "workspace:*",
    "@motiadev/core": "workspace:*"
  }
}

Create src/plugin.ts to define your plugin:

import type { MotiaPlugin, MotiaPluginContext } from '@motiadev/core'
 
export default function plugin(motia: MotiaPluginContext): MotiaPlugin {
  return {
    workbench: [
      {
        packageName: '@motiadev/plugin-example',
        cssImports: ['@motiadev/plugin-example/dist/plugin-example.css'],
        label: 'Example',
        position: 'bottom',
        componentName: 'ExamplePage',
        labelIcon: 'sparkles',
      },
    ],
  }
}

Create your React component in src/components/example-page.tsx:

import { Badge, Button, Card } from '@motiadev/ui'
import { Sparkles } from 'lucide-react'
import type React from 'react'
 
export const ExamplePage: React.FC = () => {
  return (
    <div className="h-full w-full p-6 overflow-auto">
      <div className="max-w-4xl mx-auto space-y-6">
        <div className="flex items-center gap-3">
          <Sparkles className="w-8 h-8 text-primary" />
          <h1 className="text-3xl font-bold">Example Plugin</h1>
          <Badge variant="info">v1.0.0</Badge>
        </div>
 
        <Card className="p-6">
          <h2 className="text-xl font-semibold mb-4">Welcome!</h2>
          <p className="text-muted-foreground">
            This is your custom plugin content.
          </p>
        </Card>
      </div>
    </div>
  )
}

Create src/index.ts to export your components:

import './styles.css'
 
export { ExamplePage } from './components/example-page'

Create src/styles.css to import Motia's UI styles:

@import "@motiadev/ui/globals.css";
@import "tailwindcss";

Vite configuration — Create vite.config.ts:

import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
 
export default defineConfig({
  plugins: [react(), tailwindcss(), dts({ insertTypesEntry: true })],
  build: {
    lib: {
      entry: {
        index: resolve(__dirname, 'src/index.ts'),
        plugin: resolve(__dirname, 'src/plugin.ts'),
      },
      name: 'MotiaPluginExample',
      formats: ['es', 'cjs'],
      fileName: (format, entryName) => 
        `${entryName}.${format === 'es' ? 'js' : 'cjs'}`,
    },
    rollupOptions: {
      external: ['react', 'react-dom', '@motiadev/core', '@motiadev/ui'],
    },
    cssCodeSplit: false,
  },
})

PostCSS configuration — Create postcss.config.js:

export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

TypeScript configuration — Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src"],
  "exclude": ["dist", "node_modules"]
}

Add build scripts to your package.json:

{
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch",
    "clean": "rm -rf dist"
  }
}

Then run the build:

pnpm run build

Local Plugins

Local plugins provide a simpler alternative to creating full distributable packages when you want to add custom functionality specific to your project. Unlike publishable plugins, local plugins don't require building, packaging, or separate dependencies—they live directly in your project directory.

When to Use Local Plugins

Local plugins are ideal for:

Development & prototyping

Quickly test plugin ideas without the overhead of package setup.

Project-specific features

Ship custom functionality that only matters for your project.

Internal tooling

Build dashboards, monitors, or utilities tailored to your team.

Learning environment

Understand how plugins work before creating a distributable package.

The ~/ Package Name Syntax

Local package resolution with ~/

The ~/ prefix in packageName tells Motia to load components from your local project directory instead of node_modules:

workbench: [
  {
    packageName: '~/plugins',  // Loads from <project-root>/plugins
    componentName: 'Plugin',
    // ...
  }
]

Motia resolves ~/ to your project root, so you can import components without publishing them as registry packages.

Creating a Local Plugin

Create a simple structure in your project:

index.tsx
motia.config.ts

In motia.config.ts, create a plugin function that returns the plugin configuration:

import path from 'node:path'
import { config, type MotiaPlugin, type MotiaPluginContext } from '@motiadev/core'
 
function localPluginExample(motia: MotiaPluginContext): MotiaPlugin {
  // Register custom API endpoint
  motia.registerApi(
    {
      method: 'GET',
      path: '/__motia/local-plugin-example',
    },
    async (req, ctx) => {
      return {
        status: 200,
        body: {
          message: 'Hello from Motia Plugin!',
          timestamp: new Date().toISOString(),
          environment: process.env.NODE_ENV || 'development',
          status: 'active',
        },
      }
    },
  )
 
  return {
    dirname: path.join(__dirname, 'plugins'),
    steps: ['**/*.step.ts', '**/*_step.py'],
    workbench: [
      {
        componentName: 'Plugin',
        packageName: '~/plugins',  // Load from local project
        label: 'Local Plugin Example',
        position: 'top',
        labelIcon: 'toy-brick',
      },
    ],
  }
}
 
export default config({
  plugins: [localPluginExample],
})

Create plugins/index.tsx with your component:

import { Badge, Button, cn } from '@motiadev/ui'
import { AlertCircle, CheckCircle2, Clock, RefreshCw, Server } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
 
interface PluginData {
  message: string
  timestamp: string
  environment: string
  status: 'active' | 'inactive'
}
 
export const Plugin = () => {
  const [data, setData] = useState<PluginData | null>(null)
  const [isLoading, setIsLoading] = useState(true)
 
  const fetchData = useCallback(async () => {
    setIsLoading(true)
    try {
      const response = await fetch('/__motia/local-plugin-example')
      const result = await response.json()
      setData(result)
    } catch (err) {
      console.error('Failed to fetch data:', err)
    } finally {
      setIsLoading(false)
    }
  }, [])
 
  useEffect(() => {
    fetchData()
  }, [fetchData])
 
  return (
    <div className="h-full flex flex-col p-4 gap-4">
      <div className="flex items-center justify-between border-b pb-4">
        <div className="flex items-center gap-3">
          <Server className="w-5 h-5 text-accent-1000" />
          <h1 className="text-xl font-semibold">Local Plugin Example</h1>
        </div>
        <Button onClick={fetchData} disabled={isLoading}>
          <RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
          Refresh
        </Button>
      </div>
      
      {data && (
        <div className="space-y-4">
          <div className="p-4 rounded-lg border bg-card">
            <Badge variant={data.status === 'active' ? 'default' : 'secondary'}>
              {data.status}
            </Badge>
            <p className="text-2xl font-semibold mt-2">{data.message}</p>
          </div>
        </div>
      )}
    </div>
  )
}

Including Custom Steps

Include custom steps

Local plugins can also include custom steps by specifying a dirname and steps pattern:

return {
  dirname: path.join(__dirname, 'plugins'),
  steps: ['**/*.step.ts', '**/*_step.py'],
  workbench: [/* ... */],
}

This loads API routes, event handlers, and other step types directly from the plugin directory.

Best Practices for Local Plugins

Keep components simple

Use Motia UI components directly without extra build tooling.

Use TypeScript

Leverage type safety and IDE support without extra declaration files.

Organize by feature

Group related components and steps inside meaningful folders.

Reuse dependencies

Rely on packages already present in your project workspace.

Document your APIs

Add clear comments for every custom endpoint and interface.

When to Migrate to NPM Plugin

When to publish as a distributable package

  • You want to share it across multiple projects
  • It provides general-purpose functionality
  • You need versioning and dependency management
  • The plugin is stable and well-tested

Using Plugins

Installing a Plugin

pnpm add @motiadev/plugin-example
import examplePlugin from '@motiadev/plugin-example/plugin'
 
export default {
  plugins: [examplePlugin],
}

Configuring Multiple Plugins

Configure multiple plugins

import logsPlugin from '@motiadev/plugin-logs/plugin'
import endpointPlugin from '@motiadev/plugin-endpoint/plugin'
import examplePlugin from '@motiadev/plugin-example/plugin'
 
export default {
  plugins: [
    logsPlugin,
    endpointPlugin,
    examplePlugin,
  ],
}

Registering Custom APIs

Register custom APIs

Use the registerApi method from the plugin context:

import { MotiaPlugin } from './app-config-types'
export default function plugin(motia: MotiaPluginContext): MotiaPlugin {
  // Register a custom API endpoint
  motia.registerApi(
    {
      method: 'GET',
      path: '/api/my-endpoint',
    },
    async (req, res) => {
      return res.json({ message: 'Hello from plugin!' })
    }
  )
 
  return {
    workbench: [/* ... */],
  }
}

Example Plugin

Example plugin reference

A complete minimal example plugin lives at plugins/plugin-example in the Motia repository. It demonstrates:

  • Basic plugin structure
  • Workbench tab integration
  • UI component creation
  • Build configuration
  • TypeScript setup

Use it as a starting point for your own plugins.

Troubleshooting

Plugin not showing in workbench

  • Check that the plugin is imported in motia.config.ts
  • Verify the componentName matches your exported component
  • Ensure the plugin is built (pnpm run build)
  • Check browser console for errors

Styles not loading

  • Verify CSS is imported in src/index.ts
  • Check that styles.css is exported in package.json
  • Ensure TailwindCSS is properly configured
  • Confirm that cssImports is defined in src/plugin.ts with the path to the built CSS file (e.g., ['@motiadev/plugin-example/dist/plugin-example.css'])

Resolving type errors

  • Make sure @motiadev/core and @motiadev/ui are listed in peerDependencies
  • Run pnpm install so TypeScript picks up the types
  • Confirm declaration: true is set in tsconfig.json

Next Steps

Need help? See our Community Resources for questions, examples, and discussions.