Skip to content

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

json
{
  "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:

text
===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

SectionPurpose
===public-config===Declares the entry file and optional SPA route rules
===public-file:path===Adds a static file to the public bundle

public-config

FieldTypeDescription
entrystringEntry file served for the root URL, usually index.html
routesarrayOptional route rules for SPA fallback
routes[].pathstringRoute prefix
routes[].spabooleanIf true, unmatched paths under the route serve the entry file

Authoring Limits

LimitValue
Max public files50
Max file size512 KB per file
Max total bundle size2 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

TriggerAgent TypesDescription
"always"Skills (always-on)Active in every conversation
"auto"Tools, Connectors, Skills (on-demand)AI decides when to call
"manual"AnyUser must explicitly invoke

Always-On Skills (trigger: "always")

json
{
  "trigger": "always"
}

Always-on skills have no tool definition — their prompt is injected into every conversation.

On-Demand Skills / Prompt Tools (trigger: "auto" + tool)

json
{
  "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")

json
{
  "trigger": "auto",
  "tool": {
    "name": "get_weather",
    "description": "Get current weather for a city",
    "parameters": { ... }
  }
}

Connectors (trigger: "auto" + oauth)

json
{
  "trigger": "auto",
  "oauth": { ... },
  "tool": { ... }
}

OAuth Configuration

Built-in Providers

PIE has built-in support for:

ProviderOAuth URLsNotes
googleAutomaticGmail, Drive, Calendar, etc.
githubAutomaticRepositories, Issues, etc.
slackAutomaticWorkspaces, Channels, etc.
notionAutomaticPages, Databases, etc.

For built-in providers, only specify scopes:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

js
async function handler(input, context) {
  const { topics, maxResults, frequency } = context.userConfig;
  // Use the user's configured values
}

Field Types

TypeDescriptionExtra Options
textSingle-line text inputplaceholder
numberNumeric inputmin, max
booleanToggle switch-
selectDropdown menuoptions: [{ value, label }]
tagsArray of stringsplaceholder

Field Options

OptionTypeDescription
typestringRequired. One of: text, number, boolean, select, tags
labelstringRequired. Display label for the field
descriptionstringHelp text shown below the field
defaultanyDefault value (type must match field type)
requiredbooleanWhether the field is required
placeholderstringPlaceholder text (text, tags)
minnumberMinimum value (number)
maxnumberMaximum value (number)
optionsarrayOptions for select: [{ value: string, label: string }]

Example: News Digest Agent

json
{
  "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[].events declares 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.

json
{
  "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

FieldTypeDescription
idstringStable event identifier stored on the user's heartbeat schedule. Use lowercase snake_case or kebab-case.
displayNamestringFriendly label shown in the Heartbeats UI.
descriptionstringOptional help text shown to developers and users.
enabledbooleanOptional. Set to false to keep the event in the manifest but hide it from users.
matchersarrayOne 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:

json
{
  "runtime": {
    "persistent": true,
    "timeoutMs": 600000,
    "maxSessionAge": 86400000
  }
}

Properties

PropertyTypeDefaultDescription
timeoutMsnumber120000Maximum execution time per invocation in milliseconds. Min: 30000, Max: 600000.
persistentbooleanfalseEnable pause/resume sandbox sessions. When true, the sandbox is paused after each invocation instead of killed, preserving filesystem and process state across messages.
maxSessionAgenumber86400000Maximum 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: true in session metadata, or sessions are automatically cleaned up after maxSessionAge.

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

json
{
  "trigger": "auto",
  "runtime": {
    "timeoutMs": 300000
  },
  "tool": {
    "name": "video_processor",
    "description": "Process and transcode video files",
    "parameters": { ... }
  }
}

Example: Persistent coding agent

json
{
  "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.

json
{
  "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.

json
{
  "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

FieldTypeDescription
columnsRecord<string, string>Required. Map of column name to column definition
rlsstringOptional. 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:

ExampleDescription
"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:

  1. Enables Row-Level Security on the table
  2. Forces RLS for the table owner
  3. 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

ScenarioAction
Table in manifest, not in DBCREATE TABLE
Column in manifest, not on existing tableALTER TABLE ADD COLUMN
Table/column in DB, not in manifestIgnored (no drops)
Column exists with different typeWarning (no alter)

Examples

Simple table:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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

TypeJSON SchemaExample
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:

json
{
  "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

json
{
  "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:

json
{
  "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:

json
{
  "automation": {
    "triggers": [{ "type": "webhook" }],
    "onWebhook": true
  }
}

Your agent can then export:

js
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").

json
{
  "automation": {
    "triggers": [
      {
        "type": "webhook",
        "eventTypeField": "type",
        "events": [
          { "name": "invoice.paid", "description": "Invoice successfully paid" },
          { "name": "charge.failed", "description": "A charge attempt failed" }
        ]
      }
    ],
    "onWebhook": true
  }
}
FieldTypeDescription
eventTypeFieldstringDot-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.
eventsarrayList 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:

json
{
  "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

TypeDescriptionConfiguration
cronSchedule via cron expressiondefault: "0 7 * * *"
webhookExternal HTTP triggerAuto-generates URL. Optionally add eventTypeField + events[] to declare event types.
intervalFixed intervaleveryMs: 3600000

Cron Expressions

Common patterns:

ExpressionMeaning
0 7 * * *Daily at 7am
*/15 * * * *Every 15 minutes
0 9 * * 1-5Weekdays 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.

json
{
  "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

json
{
  "trigger": "always"
}

On-Demand Skill (Prompt Tool) Manifest

json
{
  "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

json
{
  "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

json
{
  "automation": {
    "triggers": [
      {
        "type": "webhook"
      }
    ],
    "timeout": 30
  }
}

Combined Tool + Automation

json
{
  "trigger": "auto",
  "tool": {
    "name": "get_weather",
    "description": "Get current weather"
  },
  "automation": {
    "triggers": [
      {
        "type": "cron",
        "default": "0 7 * * *"
      }
    ]
  }
}

Tool Manifest

json
{
  "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

json
{
  "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.

json
{
  "machineCapabilities": [
    "machine.info",
    "clipboard.read",
    "notifications.send",
    "messages.read"
  ]
}

Available capabilities:

CapabilityRiskDescription
machine.infoLowRead hostname, OS, uptime
clipboard.readMediumRead clipboard contents
notifications.sendLowSend desktop notifications
messages.readHighRead iMessages (macOS)
shell.runCriticalExecute shell commands

See the Machine Capabilities Guide for details on using the context.machine API in your plugin code.

Built with VitePress