Manifest Schema
The manifest defines your agent's behavior, triggers, and capabilities.
Security Scanning
All plugins are automatically security scanned when created or updated. The scan checks your code and manifest against 10 security criteria. Network requests are verified against your plugin's stated purpose, so make sure your manifest accurately describes what your plugin does.
Full Schema
{
"trigger": "always" | "auto" | "manual",
"oauth": {
"provider": "google" | "github" | "slack" | "notion" | "custom",
"providerName": "string (for custom providers)",
"authorizationUrl": "string (for custom providers)",
"tokenUrl": "string (for custom providers)",
"userInfoUrl": "string (optional)",
"revokeUrl": "string (optional)",
"scopes": ["string"],
"clientIdSecret": "string (key in developerSecrets)",
"clientSecretSecret": "string (key in developerSecrets)"
},
"developerSecrets": {
"SECRET_NAME": {
"description": "string",
"required": boolean
}
},
"userSecrets": {
"SECRET_NAME": {
"description": "string",
"required": boolean
}
},
"database": {
"tables": {
"table_name": {
"columns": {
"column_name": "column_definition (type + constraints + default)"
},
"rls": "column_name (optional, enables Row-Level Security)"
}
}
},
"runtime": {
"persistent": boolean,
"timeoutMs": number,
"maxSessionAge": number
},
"tool": {
"name": "string",
"description": "string",
"parameters": {
"type": "object",
"properties": {
"param_name": {
"type": "string" | "number" | "boolean" | "array" | "object",
"description": "string",
"enum": ["value1", "value2"] // optional
}
},
"required": ["param_name"]
}
}
}Public App Packaging in .pie Files
Public apps are authored in fenced sections of the .pie file. They are not declared inside the JSON manifest object itself.
Use these sections:
===public-config===
entry: index.html
routes:
- path: /
spa: true
===public-config===
===public-file:index.html===
<!DOCTYPE html>
<html>...</html>
===public-file:index.html===
===public-file:app.js===
console.log('hello');
===public-file:app.js===Public App Fields
| Section | Purpose |
|---|---|
===public-config=== | Declares the entry file and optional SPA route rules |
===public-file:path=== | Adds a static file to the public bundle |
public-config
| Field | Type | Description |
|---|---|---|
entry | string | Entry file served for the root URL, usually index.html |
routes | array | Optional route rules for SPA fallback |
routes[].path | string | Route prefix |
routes[].spa | boolean | If true, unmatched paths under the route serve the entry file |
Authoring Limits
| Limit | Value |
|---|---|
| Max public files | 50 |
| Max file size | 512 KB per file |
| Max total bundle size | 2 MB |
Paths must be relative, must not start with /, and must not contain ...
See the Public Apps and Routes guide for full examples, hosted URL structure, public actions, uploads, and custom domains.
Trigger Types
| Trigger | Agent Types | Description |
|---|---|---|
"always" | Skills (always-on) | Active in every conversation |
"auto" | Tools, Connectors, Skills (on-demand) | AI decides when to call |
"manual" | Any | User must explicitly invoke |
Always-On Skills (trigger: "always")
{
"trigger": "always"
}Always-on skills have no tool definition — their prompt is injected into every conversation.
On-Demand Skills / Prompt Tools (trigger: "auto" + tool)
{
"trigger": "auto",
"tool": {
"name": "analyze_nda",
"description": "Analyze an NDA for risks and unusual clauses",
"parameters": {
"type": "object",
"properties": {
"document_text": {
"type": "string",
"description": "The full NDA text to analyze"
}
},
"required": ["document_text"]
}
}
}On-demand skills (prompt tools) are tier: skill plugins that include a tool: definition. They have no executable code — when the AI calls the tool, the skill's prompt template is returned as the tool result. This is ideal for domain expertise and large knowledge bases that shouldn't be in every conversation.
Tools (trigger: "auto")
{
"trigger": "auto",
"tool": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": { ... }
}
}Connectors (trigger: "auto" + oauth)
{
"trigger": "auto",
"oauth": { ... },
"tool": { ... }
}OAuth Configuration
Built-in Providers
PIE has built-in support for:
| Provider | OAuth URLs | Notes |
|---|---|---|
google | Automatic | Gmail, Drive, Calendar, etc. |
github | Automatic | Repositories, Issues, etc. |
slack | Automatic | Workspaces, Channels, etc. |
notion | Automatic | Pages, Databases, etc. |
For built-in providers, only specify scopes:
{
"oauth": {
"provider": "google",
"scopes": [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.send"
],
"clientIdSecret": "GOOGLE_CLIENT_ID",
"clientSecretSecret": "GOOGLE_CLIENT_SECRET"
}
}Custom Providers
For any other OAuth 2.0 service:
{
"oauth": {
"provider": "custom",
"providerName": "Dropbox",
"authorizationUrl": "https://www.dropbox.com/oauth2/authorize",
"tokenUrl": "https://api.dropboxapi.com/oauth2/token",
"scopes": ["files.content.read"],
"clientIdSecret": "DROPBOX_CLIENT_ID",
"clientSecretSecret": "DROPBOX_CLIENT_SECRET"
}
}Developer Secrets
Secrets are encrypted and stored securely. Define them in the manifest:
{
"developerSecrets": {
"API_KEY": {
"description": "Your service API key",
"required": true
},
"OPTIONAL_SECRET": {
"description": "Optional configuration",
"required": false
}
}
}Access in code via context.secrets.API_KEY.
User-Configurable Fields
Allow users to customize agent behavior through a settings UI. Define fields in your manifest:
{
"userFields": {
"topics": {
"type": "tags",
"label": "News Topics",
"description": "Topics you want to track",
"default": ["technology", "business"],
"required": true
},
"maxResults": {
"type": "number",
"label": "Max Results",
"min": 1,
"max": 10,
"default": 5
},
"emailDigest": {
"type": "boolean",
"label": "Send Email Digest",
"default": false
},
"frequency": {
"type": "select",
"label": "Frequency",
"options": [
{ "value": "daily", "label": "Daily" },
{ "value": "weekly", "label": "Weekly" }
],
"default": "daily"
}
}
}Access in code via context.userConfig:
async function handler(input, context) {
const { topics, maxResults, frequency } = context.userConfig;
// Use the user's configured values
}Field Types
| Type | Description | Extra Options |
|---|---|---|
text | Single-line text input | placeholder |
number | Numeric input | min, max |
boolean | Toggle switch | - |
select | Dropdown menu | options: [{ value, label }] |
tags | Array of strings | placeholder |
Field Options
| Option | Type | Description |
|---|---|---|
type | string | Required. One of: text, number, boolean, select, tags |
label | string | Required. Display label for the field |
description | string | Help text shown below the field |
default | any | Default value (type must match field type) |
required | boolean | Whether the field is required |
placeholder | string | Placeholder text (text, tags) |
min | number | Minimum value (number) |
max | number | Maximum value (number) |
options | array | Options for select: [{ value: string, label: string }] |
Example: News Digest Agent
{
"automation": {
"triggers": [{ "type": "cron", "default": "0 8 * * *" }]
},
"userFields": {
"topics": {
"type": "tags",
"label": "News Topics",
"description": "Topics you want to track (e.g., AI, climate, startups)",
"default": ["technology", "business"],
"required": true
},
"maxArticles": {
"type": "number",
"label": "Articles per Topic",
"min": 1,
"max": 10,
"default": 3
},
"timeRange": {
"type": "select",
"label": "Time Range",
"options": [
{ "value": "day", "label": "Last 24 Hours" },
{ "value": "week", "label": "Past Week" }
],
"default": "day"
}
},
"developerSecrets": {
"TAVILY_API_KEY": { "description": "Tavily API key", "required": true }
}
}heartbeatEvents Object
Use heartbeatEvents to publish a curated catalog of plugin activity that users can subscribe to from Heartbeats.
This is different from automation.triggers[].events:
automation.triggers[].eventsdeclares raw webhook event types for automation plugins.heartbeatEvents.events[]declares developer-approved, user-facing events backed by exact tool, widget, public-action, or webhook matchers.
PIE can suggest heartbeat events automatically when you save a plugin, but the manifest catalog is the source of truth that users actually see.
{
"heartbeatEvents": {
"events": [
{
"id": "submission_received",
"displayName": "New form submission",
"description": "Fires when a visitor submits the public form.",
"enabled": true,
"matchers": [
{ "source": "public", "actionId": "submit_form" }
]
},
{
"id": "form_created",
"displayName": "Form created",
"description": "Fires when a new form is created from chat.",
"enabled": true,
"matchers": [
{ "source": "tool", "action": "create_form" }
]
}
]
}
}heartbeatEvents.events[] fields
| Field | Type | Description |
|---|---|---|
id | string | Stable event identifier stored on the user's heartbeat schedule. Use lowercase snake_case or kebab-case. |
displayName | string | Friendly label shown in the Heartbeats UI. |
description | string | Optional help text shown to developers and users. |
enabled | boolean | Optional. Set to false to keep the event in the manifest but hide it from users. |
matchers | array | One or more exact runtime matchers that should fire this curated event. |
Matcher Shapes
heartbeatEvents supports these exact matcher objects:
{ "source": "tool", "action": "create_form" }{ "source": "widget", "actionId": "publish_form" }{ "source": "public", "actionId": "submit_form" }{ "source": "webhook", "eventName": "invoice.paid" }
Use multiple matchers when different surfaces represent the same business outcome. For example, a "Blog post published" event might match both a chat tool action and a widget action.
Runtime Configuration
Control how your agent's sandbox behaves at runtime. Add a runtime block to your manifest:
{
"runtime": {
"persistent": true,
"timeoutMs": 600000,
"maxSessionAge": 86400000
}
}Properties
| Property | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | 120000 | Maximum execution time per invocation in milliseconds. Min: 30000, Max: 600000. |
persistent | boolean | false | Enable pause/resume sandbox sessions. When true, the sandbox is paused after each invocation instead of killed, preserving filesystem and process state across messages. |
maxSessionAge | number | 86400000 | Maximum total lifetime of a persistent session in milliseconds before auto-cleanup. Only applies when persistent: true. |
Persistent Sandboxes
When persistent is true, PIE manages the sandbox lifecycle differently:
- First invocation: A new sandbox is created from the assigned template.
- After execution: The sandbox is paused (not killed). Billing stops, but filesystem and memory state are preserved.
- Subsequent invocations: The paused sandbox is resumed instantly, picking up exactly where it left off.
- Session end: The plugin can request a kill by setting
requestKill: truein session metadata, or sessions are automatically cleaned up aftermaxSessionAge.
This is ideal for agents that need to maintain state across multiple messages — for example, a coding agent that keeps a cloned repository, or a database agent that maintains a connection pool.
See Persistent Sandboxes Guide for a full walkthrough.
Example: Long-running tool
{
"trigger": "auto",
"runtime": {
"timeoutMs": 300000
},
"tool": {
"name": "video_processor",
"description": "Process and transcode video files",
"parameters": { ... }
}
}Example: Persistent coding agent
{
"trigger": "auto",
"runtime": {
"persistent": true,
"timeoutMs": 600000,
"maxSessionAge": 86400000
},
"userSecrets": {
"GITHUB_TOKEN": {
"description": "GitHub PAT with repo scope",
"required": true
}
},
"tool": {
"name": "code_agent",
"description": "AI coding agent with persistent sandbox",
"parameters": { ... }
}
}User Secrets
Secrets that the end user provides when they install your agent. Unlike developer secrets (which you set once), user secrets are configured per-user through the agent settings UI.
{
"userSecrets": {
"API_KEY": {
"description": "Your personal API key from the service",
"required": true
},
"OPTIONAL_TOKEN": {
"description": "Optional access token for premium features",
"required": false
}
}
}Access in code via context.secrets.API_KEY (user secrets are merged with developer secrets).
Database Schema
Define database tables declaratively. When you save a plugin with a database section, PIE auto-provisions the database (if needed) and syncs the schema. The sync is additive only — it creates tables and adds columns, but never drops or alters existing objects.
{
"database": {
"tables": {
"notes": {
"columns": {
"id": "uuid primary key default gen_random_uuid()",
"pie_user_id": "text not null default current_setting('app.user_id', true)",
"title": "text not null",
"content": "text",
"created_at": "timestamptz default now()"
},
"rls": "pie_user_id"
}
}
}
}Table Definition
| Field | Type | Description |
|---|---|---|
columns | Record<string, string> | Required. Map of column name to column definition |
rls | string | Optional. Column name for Row-Level Security. When set, PIE auto-creates an RLS policy filtering by app.user_id |
Column Definitions
Column values are raw Postgres column definitions. You can use any valid Postgres syntax:
| Example | Description |
|---|---|
"text" | Simple text column |
"text not null" | Required text column |
"uuid primary key default gen_random_uuid()" | UUID primary key with auto-generation |
"integer not null default 0" | Integer with default value |
"timestamptz default now()" | Timestamp defaulting to current time |
"jsonb" | JSON column |
"boolean not null default false" | Boolean with default |
"text not null default current_setting('app.user_id', true)" | Auto-populated with end-user ID |
RLS Behavior
When rls is set on a table, PIE automatically:
- Enables Row-Level Security on the table
- Forces RLS for the table owner
- Creates a policy: rows are filtered where
<rls_column> = current_setting('app.user_id', true)
This means queries through context.managedDb.query() automatically return only the current user's data.
Sync Behavior
| Scenario | Action |
|---|---|
| Table in manifest, not in DB | CREATE TABLE |
| Column in manifest, not on existing table | ALTER TABLE ADD COLUMN |
| Table/column in DB, not in manifest | Ignored (no drops) |
| Column exists with different type | Warning (no alter) |
Examples
Simple table:
{
"database": {
"tables": {
"items": {
"columns": {
"id": "uuid primary key default gen_random_uuid()",
"name": "text not null",
"data": "jsonb",
"created_at": "timestamptz default now()"
}
}
}
}
}Table with RLS:
{
"database": {
"tables": {
"user_notes": {
"columns": {
"id": "uuid primary key default gen_random_uuid()",
"pie_user_id": "text not null default current_setting('app.user_id', true)",
"title": "text not null",
"content": "text",
"created_at": "timestamptz default now()"
},
"rls": "pie_user_id"
}
}
}
}Multi-table manifest:
{
"database": {
"tables": {
"projects": {
"columns": {
"id": "uuid primary key default gen_random_uuid()",
"pie_user_id": "text not null default current_setting('app.user_id', true)",
"name": "text not null",
"description": "text",
"created_at": "timestamptz default now()"
},
"rls": "pie_user_id"
},
"tasks": {
"columns": {
"id": "uuid primary key default gen_random_uuid()",
"pie_user_id": "text not null default current_setting('app.user_id', true)",
"project_id": "uuid not null",
"title": "text not null",
"status": "text not null default 'pending'",
"created_at": "timestamptz default now()"
},
"rls": "pie_user_id"
}
}
}
}Foreign Keys
Foreign keys are not auto-managed from the manifest in v1 (due to table ordering complexity), but you can create them manually via a direct connection or context.managedDb.query().
Tool Parameters
The parameters object uses JSON Schema format:
{
"tool": {
"name": "search_emails",
"description": "Search emails by query",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (e.g., 'from:john subject:meeting')"
},
"maxResults": {
"type": "integer",
"description": "Maximum results to return (default: 10)"
},
"folder": {
"type": "string",
"enum": ["inbox", "sent", "drafts", "trash"],
"description": "Which folder to search"
}
},
"required": ["query"]
}
}
}Supported Types
| Type | JSON Schema | Example |
|---|---|---|
| String | "string" | "hello" |
| Number | "number" or "integer" | 42, 3.14 |
| Boolean | "boolean" | true, false |
| Array | "array" | [1, 2, 3] |
| Object | "object" | { "key": "value" } |
Enums
Restrict values to a predefined set:
{
"action": {
"type": "string",
"enum": ["search", "read", "send", "delete"],
"description": "The action to perform"
}
}Automation Configuration
Automations run on schedules or in response to webhooks, not AI triggers.
automation Object
{
"automation": {
"triggers": [
{
"type": "cron" | "webhook" | "interval",
"default": "string (cron expression)",
"everyMs": "number (for interval type)",
"description": "string",
"eventTypeField": "string (webhook only, dot-path to event type in payload)",
"events": [
{ "name": "string", "description": "string" }
]
}
],
"timeout": 120,
"onWebhook": true,
"allowManualRun": true
}
}allowManualRun Flag
Set allowManualRun: true to show a "Run Now" button in the agent settings. This allows users to manually trigger the automation on demand:
{
"automation": {
"triggers": [{ "type": "cron", "default": "0 8 * * *" }],
"allowManualRun": true
}
}When triggered manually, the handler receives input.triggeredBy === 'manual'.
onWebhook Flag
Set onWebhook: true to indicate your agent exports an onWebhook handler for processing incoming webhook payloads:
{
"automation": {
"triggers": [{ "type": "webhook" }],
"onWebhook": true
}
}Your agent can then export:
async function onWebhook(input, context) {
const payload = input.triggerData;
// Process webhook payload
}
module.exports = { handler, onWebhook };Declaring Webhook Events
If your agent receives webhooks from an external service, you can declare the specific event types your webhook produces. This enables users to create heartbeats that trigger only on specific events (e.g., "only when a PR is merged" or "only when an invoice fails").
{
"automation": {
"triggers": [
{
"type": "webhook",
"eventTypeField": "type",
"events": [
{ "name": "invoice.paid", "description": "Invoice successfully paid" },
{ "name": "charge.failed", "description": "A charge attempt failed" }
]
}
],
"onWebhook": true
}
}| Field | Type | Description |
|---|---|---|
eventTypeField | string | Dot-path to the field in the webhook payload that identifies the event type. For example, "type" for Stripe, "event.type" for Slack, or "_headers.x-github-event" for services that send the event type in an HTTP header. |
events | array | List of event types this webhook can produce. Each entry has a name (matched against the payload value) and an optional description (shown in the UI). |
When users create a heartbeat with a "Plugin Event" trigger, the events you declare here populate the dropdown they see, so include clear descriptions.
Header-based event types: Some services (like GitHub) put the event type in an HTTP header rather than the payload body. The webhook system automatically injects selected request headers into input.triggerData._headers, so you can reference them with a dot-path like _headers.x-github-event:
{
"automation": {
"triggers": [
{
"type": "webhook",
"eventTypeField": "_headers.x-github-event",
"events": [
{ "name": "push", "description": "Push to a branch" },
{ "name": "pull_request", "description": "Pull request opened, closed, or merged" },
{ "name": "issues", "description": "Issue opened, closed, or edited" }
]
}
],
"onWebhook": true
}
}Trigger Types
| Type | Description | Configuration |
|---|---|---|
cron | Schedule via cron expression | default: "0 7 * * *" |
webhook | External HTTP trigger | Auto-generates URL. Optionally add eventTypeField + events[] to declare event types. |
interval | Fixed interval | everyMs: 3600000 |
Cron Expressions
Common patterns:
| Expression | Meaning |
|---|---|
0 7 * * * | Daily at 7am |
*/15 * * * * | Every 15 minutes |
0 9 * * 1-5 | Weekdays at 9am |
0 0 1 * * | First day of month |
Note: Cron times use the user's configured timezone.
Timeout
Maximum execution time in seconds. Default: 60. Maximum: 300.
{
"automation": {
"timeout": 120
}
}Sandbox Template
If your agent needs custom npm packages or system dependencies, assign a sandbox template via the Developer Portal UI (Settings tab > Sandbox Template dropdown). Templates are not specified in the manifest — they are managed separately in the Templates tab. See Context API - Sandbox Templates for details.
Complete Examples
Always-On Skill Manifest
{
"trigger": "always"
}On-Demand Skill (Prompt Tool) Manifest
{
"trigger": "auto",
"tool": {
"name": "analyze_nda",
"description": "Analyze an NDA for risks, unusual clauses, and missing protections",
"parameters": {
"type": "object",
"properties": {
"document_text": {
"type": "string",
"description": "The full NDA text to analyze"
},
"focus_areas": {
"type": "string",
"description": "Optional: specific concerns to focus on"
}
},
"required": ["document_text"]
}
}
}Automation Manifest
{
"automation": {
"triggers": [
{
"type": "cron",
"default": "0 7 * * *",
"description": "Daily at 7am"
}
],
"timeout": 60
},
"oauth": {
"provider": "google",
"scopes": ["https://www.googleapis.com/auth/gmail.readonly"],
"clientIdSecret": "GOOGLE_CLIENT_ID",
"clientSecretSecret": "GOOGLE_CLIENT_SECRET"
},
"developerSecrets": {
"GOOGLE_CLIENT_ID": { "required": true },
"GOOGLE_CLIENT_SECRET": { "required": true }
}
}Webhook Automation Manifest
{
"automation": {
"triggers": [
{
"type": "webhook"
}
],
"timeout": 30
}
}Combined Tool + Automation
{
"trigger": "auto",
"tool": {
"name": "get_weather",
"description": "Get current weather"
},
"automation": {
"triggers": [
{
"type": "cron",
"default": "0 7 * * *"
}
]
}
}Tool Manifest
{
"trigger": "auto",
"developerSecrets": {
"WEATHER_API_KEY": {
"description": "API key from weatherapi.com",
"required": true
}
},
"tool": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
}
},
"required": ["city"]
}
}
}Connector Manifest
{
"trigger": "auto",
"oauth": {
"provider": "google",
"scopes": [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.send"
],
"clientIdSecret": "GOOGLE_CLIENT_ID",
"clientSecretSecret": "GOOGLE_CLIENT_SECRET"
},
"developerSecrets": {
"GOOGLE_CLIENT_ID": {
"description": "Google OAuth Client ID",
"required": true
},
"GOOGLE_CLIENT_SECRET": {
"description": "Google OAuth Client Secret",
"required": true
}
},
"tool": {
"name": "gmail",
"description": "Access Gmail",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search", "read", "send"],
"description": "Gmail action"
}
},
"required": ["action"]
}
}
}Marketplace Discovery
When your plugin is published with public or official visibility, its description and tags are indexed into a vector store used for marketplace tool suggestions. During chat, PIE searches this store to recommend relevant plugins to users who don't have them installed.
To maximize your plugin's discoverability:
description: Write a clear, specific description of what your plugin does. Include key use cases and capabilities. Avoid vague descriptions like "A useful tool" — instead write "Create, read, and manipulate Excel spreadsheets (.xlsx) with formulas, charts, and pivot tables."tags: Add relevant keywords as tags. These are indexed alongside the description. Use specific terms:["spreadsheet", "excel", "xlsx", "data-analysis", "formulas"]rather than generic ones like["tool", "office"].
The combination of a strong description and precise tags ensures your plugin surfaces when users need it, even if they don't know it exists.
Machine Capabilities
Plugins that need to interact with the user's local machine can declare machineCapabilities in the manifest. When a user installs such a plugin, they must approve the requested capabilities.
{
"machineCapabilities": [
"machine.info",
"clipboard.read",
"notifications.send",
"messages.read"
]
}Available capabilities:
| Capability | Risk | Description |
|---|---|---|
machine.info | Low | Read hostname, OS, uptime |
clipboard.read | Medium | Read clipboard contents |
notifications.send | Low | Send desktop notifications |
messages.read | High | Read iMessages (macOS) |
shell.run | Critical | Execute shell commands |
See the Machine Capabilities Guide for details on using the context.machine API in your plugin code.