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:
- AI agent status
- build and deploy progress
- timers and background jobs
- local service health
- calendar or device status
- file, clipboard, or automation helpers
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 or DynamicLake Pro.
- Python 3 for the example plugin.
- A square PNG icon if you want the plugin to have a custom icon.
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.
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:
- Use a square PNG.
512 x 512is recommended.256 x 256is the practical minimum.- Keep the icon
1.5 MBor smaller. - Do not bake in rounded corners. DynamicLake applies the corner radius in the app and on the Market website
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:
- enable or disable the plugin
- allow or block button action callbacks
- view the plugin identifier and executable path
- dismiss active plugin activities
- show the plugin in Finder
- copy the plugin identifier
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:
- Use
24settings or fewer. idmust be1-64characters, start with a letter, and may contain letters, numbers,_, and-.titleis required and can be up to80characters.descriptioncan be up to180characters.systemImageshould be an SF Symbol name.tintcan beblue,cyan,teal,indigo,purple,pink,green,orange,yellow,red,gray, orwhite.default, when present, must match the setting type.sliderrequiresminlower thanmax;stepmust be positive and fit inside the range.selectrequires1-24unique options. Options can be strings or objects withtitleand optionalsystemImage.buttonopens anhttporhttpsURL.buttonTitlecan be up to40characters, anddestructivecan mark a destructive action.
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:
- 4-byte unsigned big-endian payload length.
- 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:
- the package folder uses the
.dynamiclakepluginextension plugin.jsonexists and is valid- the executable exists and is runnable
- the icon path, if present, points to a regular file
- package size limits are respected
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
- [ ] Install the package locally and confirm DynamicLake accepts it.
- [ ] Test the plugin in DynamicLake Pro or DynamicLake Playground.
- [ ] Keep the same
identifierand increaseversion. - [ ] Confirm package size, icon, executable, and JSON limits pass.
- [ ] Confirm settings are clear and do not request secrets through plain text.
- [ ] Prepare the package URL, hashes, release notes, and support or contact links.
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.