Create Your First Plugin

Build, package, and install a DynamicLake JSON plugin.

Overview

A DynamicLake plugin is a local executable that sends JSON messages to DynamicLake. Use a plugin when you want to build with Python, JavaScript, Go, Ruby, shell, or another runtime instead of creating a SwiftUI ExtensionKit extension.

Plugins are best for small workflow integrations:

Use a SwiftUI extension when you need fully custom UI. Use a JSON plugin when predefined components are enough.

JSON plugins use DynamicLake's predefined components, so you can start with this guide and JSONPluginAPI. Review DesignGuidelines before polishing the live activity, package icon, screenshots, and Market listing.

Learn From Open-Source Plugins

Every plugin submitted to DynamicLake Market must be open source. You can browse DynamicLake's open-source plugin examples to see complete packages, manifests, native helpers, settings, and live activity updates. They are a useful reference when you are building your first plugin or deciding how to structure a feature.

Prerequisites

You need:

DynamicLake Playground installs local .dynamiclakeplugin packages only. Use DynamicLake Pro to test DynamicLake Market installs and updates.

Plugins do not need DynamicLakeKit or SPM. A plugin only needs to send JSON over the socket path in DYNAMICLAKE_JSON_SOCKET. Swift plugin executables may optionally depend on DynamicLakeKit to reuse JSON types.

1. Create The Package

Create a folder ending in .dynamiclakeplugin.

!A DynamicLake plugin package containing plugin.json, an executable, an icon, and optional helper files.

BuildStatus.dynamiclakeplugin
  plugin.json
  icon.png
  build-status.py

The package folder is what users install. DynamicLake copies it into a managed plugin folder and launches the configured executable.

2. Add plugin.json

Every plugin package needs a plugin.json file at the package root.

{
  "schemaVersion": 1,
  "identifier": "com.example.plugins.build-status",
  "name": "Build Status",
  "version": "1.0.0",
  "developerName": "Example Studio",
  "executable": "build-status.py",
  "arguments": [],
  "icon": "icon.png",
  "autoStart": true,
  "settings": [
    {
      "id": "showCodexIcon",
      "type": "switch",
      "title": "Show Codex Icon",
      "description": "Show the Codex icon in the live activity",
      "systemImage": "terminal.fill",
      "tint": "cyan",
      "default": true
    }
  ]
}

Required fields:

| Field | Value | | --- | --- | | schemaVersion | Must be 1. | | identifier | Stable plugin identifier, 3-128 characters. Use reverse-DNS style. | | name | Display name, up to 80 characters. | | version | Plugin version, up to 40 characters. | | executable | Relative path to the executable inside the package. |

Optional fields:

| Field | Value | | --- | --- | | developerName | Creator or company name. | | arguments | Arguments passed to the executable. | | icon | Relative path to the package icon. | | autoStart | true to start automatically when DynamicLake opens. | | settings | Optional settings rows rendered by DynamicLake. |

Relative paths must stay inside the package. Absolute paths and .. path segments are rejected.

Icon requirements:

Package limits:

| Item | Limit | | --- | --- | | Archive | 7 MB max | | Extracted package folder | 20 MB max | | plugin.json | 128 KB max | | Icon | 1.5 MB max |

3. Plan Plugin Settings

DynamicLake creates a settings window for every installed plugin. The built-in plugin settings let users:

DynamicLake shows plugin settings in a fixed 520 x 620 window. The header uses the plugin icon and bold plugin name. The content is inside a vertical scroll view, so long settings lists scroll inside the same window.

Plugins can also declare custom settings in plugin.json. DynamicLake renders them with the same native settings rows used by the app, wrapped in a SettingsSectionContainer.

!Plugin settings components showing switch, slider, select, and button rows.

Supported setting types:

| Type | DynamicLake UI | | --- | --- | | switch | SettingSwitchCell | | slider | SettingSliderCell | | select | SettingsMenuWithImagesCell | | button | SettingsButtonCell |

Example:

{
  "settings": [
    {
      "id": "showCodexIcon",
      "type": "switch",
      "title": "Show Codex Icon",
      "description": "Show the Codex icon in the live activity",
      "systemImage": "terminal.fill",
      "tint": "cyan",
      "default": true
    },
    {
      "id": "staleSeconds",
      "type": "slider",
      "title": "Stale Timeout",
      "description": "Hide inactive sessions after this time",
      "min": 15,
      "max": 600,
      "step": 15,
      "suffix": "sec",
      "default": 90
    },
    {
      "id": "agentStyle",
      "type": "select",
      "title": "Agent Style",
      "systemImage": "sparkles",
      "options": [
        {
          "title": "Compact",
          "systemImage": "rectangle.compress.vertical"
        },
        {
          "title": "Detailed",
          "systemImage": "list.bullet.rectangle"
        }
      ],
      "default": "Compact"
    },
    {
      "id": "help",
      "type": "button",
      "title": "Plugin Help",
      "description": "Open setup instructions",
      "buttonTitle": "Open",
      "systemImage": "questionmark.circle",
      "url": "https://example.com/help"
    }
  ]
}

Setting rules:

Do not write settings, logs, caches, or generated files into the installed .dynamiclakeplugin package. DynamicLake verifies installed package files before running the plugin, so changing package contents after install can block the plugin until it is reinstalled.

When DynamicLake launches an installed plugin, it provides these environment variables:

| Variable | Value | | --- | --- | | DYNAMICLAKE_JSON_SOCKET | Unix domain socket path for JSON messages. | | DYNAMICLAKE_PLUGIN_IDENTIFIER | The plugin identifier from plugin.json. | | DYNAMICLAKE_PLUGIN_PACKAGE | The installed package folder path. Read package resources from here, but keep mutable settings elsewhere. | | DYNAMICLAKE_PLUGIN_SETTINGS_PATH | JSON file path containing the current plugin setting values. | | DYNAMICLAKE_PLUGIN_SETTINGS_JSON | Current plugin setting values as compact JSON. | | DYNAMICLAKE_SETTING_<SETTING_ID> | One environment variable per setting, using the uppercased setting id. CamelCase is split for readability, so showCodexIcon becomes DYNAMICLAKE_SETTING_SHOW_CODEX_ICON. | | DYNAMICLAKE_PLUGIN_FEATURES | Comma-separated protocol features this DynamicLake version supports, such as numericText,presentSneakPeek. Missing on older versions. See JSONPluginAPI — Feature Detection. |

The settings file uses this shape:

{
  "schemaVersion": 1,
  "identifier": "com.example.plugins.build-status",
  "values": {
    "showCodexIcon": true,
    "staleSeconds": 90,
    "agentStyle": "Compact"
  }
}

DynamicLake rewrites the settings file when the user changes plugin settings. Long-running plugins can watch or reread DYNAMICLAKE_PLUGIN_SETTINGS_PATH.

4. Choose Components

Plugins render DynamicLake UI with predefined components. Use these component types inside compactLiveActivity, sneakPeek.leftSlot, sneakPeek.center, and sneakPeek.rightSlot.

Choose A Priority

Use priority to decide how your live activity competes with other DynamicLake content:

| Priority | Use it for | | --- | --- | | low | Long-running, ambient activities that can wait behind more immediate content, such as calendar events and ongoing status. | | normal | The default for active work users may want to glance at, such as music playback, file uploads, builds, and downloads. | | high | Important, time-sensitive activity the user should not miss, such as calls, messages, and urgent notifications. |

Use normal unless your activity clearly fits the low- or high-priority cases. Do not use high just to make an activity more visible.

| Type | Use For | | --- | --- | | text | Sneak peek labels and marquee text. Use style: "numeric" for changing numbers such as 45% so the digits animate. In compactLiveActivity, use text only for duration-style values. | | image | SF Symbols, the plugin icon, or very small inline PNG/JPEG data. | | button | Simple actions that send an actionID back to the plugin. Keep compact buttons icon-only; text buttons belong in sneakPeek. | | progress | Determinate or indeterminate progress. | | timer | Countdown or elapsed time between ISO-8601 dates. | | status | Success, failure, in-progress, paused, or warning state. |

Most components can use tint: blue, cyan, teal, indigo, purple, pink, green, orange, yellow, red, gray, or white.

In sneakPeek, use a rounded rectangle when the button needs visible text:

{
  "type": "button",
  "title": "Stop",
  "systemImage": "xmark",
  "actionID": "stop-build",
  "shape": "roundedRect",
  "role": "destructive"
}

To get the user's attention when something finishes, add "presentSneakPeek": 2 to an update to open the sneak peek for two seconds without hovering.

Numeric text and presentSneakPeek need a recent DynamicLake version. Check DYNAMICLAKE_PLUGIN_FEATURES before using them; older versions reject the whole message.

For the full component schema, including button callbacks, inline images, animated numbers, and automatic sneak peeks, see JSONPluginAPI.

5. Set Size And Priority

Plugins can choose the live activity size and priority in each create command:

{
  "schemaVersion": 1,
  "type": "create",
  "activityID": "build",
  "priority": "normal",
  "size": "large",
  "surfaces": {
    "compactLiveActivity": {
      "leftSlot": { "type": "status", "status": "inProgress", "tint": "blue" },
      "rightSlot": { "type": "progress", "value": 0.42, "tint": "blue" }
    }
  }
}

Supported size values:

| Value | Use For | | --- | --- | | small | Icon-only, status, progress, or timer layouts that must stay tight. | | normal | Compact visual, status, progress, or duration layouts that need slightly more room than small. | | large | Widest compact activity for visual, status, progress, or duration layouts that need the most room. |

Do not put descriptive text in compactLiveActivity slots. Use text in sneakPeek; in the live activity, text should be duration-style content such as a timer or countdown.

Supported priority values:

| Value | Use For | | --- | --- | | low | Background state that should yield to normal activity. | | normal | Most plugin activity. This is the default. | | high | Time-sensitive work that should appear ahead of normal activity. |

If a plugin omits size or priority, DynamicLake uses normal. On update, send size or priority only when that value should change.

6. Write The Plugin

DynamicLake passes the local socket path to the process in DYNAMICLAKE_JSON_SOCKET. Messages are framed as:

  1. 4-byte unsigned big-endian payload length.
  2. UTF-8 JSON payload.

This minimal Python plugin creates one compact live activity and then updates progress.

The package identifier lives in plugin.json. The Python code only needs an activityID, which is the per-plugin activity instance DynamicLake should create and update.

#!/usr/bin/env python3

import json
import os
import socket
import struct
import time


def send_frame(sock, payload):
    data = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    sock.sendall(struct.pack(">I", len(data)) + data)


socket_path = os.environ["DYNAMICLAKE_JSON_SOCKET"]
ACTIVITY_ID = "build-status"

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
    sock.connect(socket_path)

    send_frame(sock, {
        "schemaVersion": 1,
        "requestID": "create-build",
        "type": "create",
        "activityID": ACTIVITY_ID,
        "title": "Build",
        "priority": "normal",
        "size": "large",
        "surfaces": {
            "compactLiveActivity": {
                "leftSlot": {
                    "type": "status",
                    "status": "inProgress",
                    "tint": "blue"
                },
                "rightSlot": {
                    "type": "progress",
                    "value": 0,
                    "tint": "blue"
                }
            },
            "sneakPeek": {
                "leftSlot": {
                    "type": "status",
                    "status": "inProgress",
                    "tint": "blue"
                },
                "center": {
                    "type": "text",
                    "text": "Building project",
                    "style": "marquee"
                }
            }
        }
    })

    for step in range(1, 11):
        send_frame(sock, {
            "schemaVersion": 1,
            "requestID": f"update-{step}",
            "type": "update",
            "activityID": ACTIVITY_ID,
            "surfaces": {
                "compactLiveActivity": {
                    "leftSlot": {
                        "type": "status",
                        "status": "inProgress",
                        "tint": "blue"
                    },
                    "rightSlot": {
                        "type": "progress",
                        "value": step / 10,
                        "tint": "blue"
                    }
                }
            }
        })
        time.sleep(0.5)

    send_frame(sock, {
        "schemaVersion": 1,
        "requestID": "complete-build",
        "type": "update",
        "activityID": ACTIVITY_ID,
        "surfaces": {
            "compactLiveActivity": {
                "leftSlot": {
                    "type": "status",
                    "status": "success",
                    "tint": "green"
                },
                "rightSlot": {
                    "type": "progress",
                    "value": 1,
                    "tint": "green"
                }
            }
        }
    })

    time.sleep(2)
    send_frame(sock, {
        "schemaVersion": 1,
        "requestID": "dismiss-build",
        "type": "dismiss",
        "activityID": ACTIVITY_ID
    })

DynamicLake also removes a plugin's active activities when that plugin disconnects. Keep the socket open while the activity should stay visible, or send dismiss when the work is done.

Make the executable runnable:

chmod +x BuildStatus.dynamiclakeplugin/build-status.py

7. Install Locally

Open DynamicLake Playground Settings > Plugins > Install Local, then choose the .dynamiclakeplugin package. DynamicLake validates the package before showing the install confirmation.

You can test the plugin with DynamicLake Playground before using the full DynamicLake app. DynamicLake Playground is free, runs one extension or plugin at a time, and is intended for local creator testing.

After install, you should see a large Build live activity. Progress fills from 0 to 100%, switches to success, then dismisses after about two seconds.

During install DynamicLake checks:

Troubleshooting:

| Problem | Fix | | --- | --- | | Choose a .dynamiclakeplugin package. | Rename the package folder so it ends in .dynamiclakeplugin. | | The plugin executable is not marked as runnable. | Run chmod +x BuildStatus.dynamiclakeplugin/build-status.py. | | Plugin files changed after install. | Reinstall the package after changing files inside it. | | DYNAMICLAKE_JSON_SOCKET is missing. | Launch the plugin from DynamicLake instead of running it directly. |

8. Update The Plugin

Keep the same identifier and increase version.

When a newer version is available from the signed DynamicLake Market catalog, DynamicLake shows an Update action. Updating preserves the user's enabled state.

9. Submit To The Market

Before submitting, do a short release check:

Create the submission archive with:

ditto -c -k --keepParent BuildStatus.dynamiclakeplugin BuildStatus.dynamiclakeplugin.zip

Submit from DynamicLake Market:

https://market.dynamiclake.com

Include a package URL when the build is ready for review. Package URLs are optional while the submission flow is being tested, but approved plugins need a downloadable package and matching hashes in the signed catalog.

For package limits, icon rules, catalog metadata, and review status emails, see PluginPackagingAndMarket.

Topics

Related Guides