Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .agents/skills/piece-builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ The condensed rules in this file (Quick Auth Reference, Quick Piece Definition T
| Shared API helper, pagination, or `createCustomApiCallAction` | `common-patterns.md` |
| An advanced UX pattern (source selectors, AWS-style auth) | `ux-guidelines.md` |
| Flattening a deeply nested API response | `output-quality.md` |
| Tagging an action/trigger for AI agents | `ai-metadata.md` |
| Tagging an action/trigger (`audience`, `aiMetadata`, `classification`) | `ai-metadata.md` |

### Step 5: WIRE & VERIFY

Expand All @@ -79,6 +79,7 @@ The condensed rules in this file (Quick Auth Reference, Quick Piece Definition T
- [ ] Import every action in `src/index.ts` → add to `actions: [...]`
- [ ] Import every trigger in `src/index.ts` → add to `triggers: [...]`
- [ ] Add `createCustomApiCallAction` to `actions: [...]`
- [ ] Every hand-written action carries `audience`, `aiMetadata`, and `classification`; every trigger carries `aiMetadata` and `classification: 'READ'` (see `ai-metadata.md`)
- [ ] Register in `tsconfig.base.json` at repo root (insert **alphabetically** — build fails without this):
```json
"@activepieces/piece-<name>": ["packages/pieces/community/<name>/src/index.ts"]
Expand Down Expand Up @@ -224,8 +225,9 @@ Full patterns and examples: read `output-quality.md`

- **`audience`** (actions only): `'human' | 'ai' | 'both'` — written explicitly on every action (`'both'` for normal integration actions; `'human'` for LLM-wrappers/utilities). Downstream filters only see it when physically present.
- **`aiMetadata`**: `{ description, idempotent }` on every action, `{ description }` on every trigger — agent-facing description (what + when-to-pick + key constraint) and safe-retry hint derived from `run()`.
- **`classification`** (actions **and** triggers): `'READ' | 'SEARCH' | 'WRITE' | 'DESTRUCTIVE'` — what the step does to external state, judged from the `run()` body (never from the name). Renders as a badge in the builder's piece selector. **Every trigger is `'READ'`.**

The catalog is fully curated; a new action or trigger without these is a regression. Writing rules, `idempotent` derivation, factory gotchas: read `ai-metadata.md`
A new action or trigger without these is a regression. Writing rules, `idempotent` derivation, the classification rubric, factory gotchas: read `ai-metadata.md`

---

Expand Down
39 changes: 34 additions & 5 deletions .agents/skills/piece-builder/ai-metadata.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# AI-Ready Metadata

Pieces power both human flow-builders and AI agents (via the MCP server and the agent tooling). Two fields declare how an action or trigger appears to agents. They are additive — they change nothing for human users — but the catalog is now fully curated (every existing action carries them), so **new actions and triggers must ship with them**: a piece authored without AI metadata is a regression that has to be backfilled later.
Pieces power both human flow-builders and AI agents (via the MCP server and the agent tooling). Three fields declare how an action or trigger appears to agents and to flow-builders. They are additive — they change nothing about execution — and **new actions and triggers must ship with all of them**: a piece authored without them is a regression that has to be backfilled later.

| Field | Where | Shape | New code |
|---|---|---|---|
| `audience` | actions only | `'human' \| 'ai' \| 'both'` | **required, written explicitly** |
| `aiMetadata` | actions **and** triggers | `{ description?: string; idempotent?: boolean }` | **required** — `{ description, idempotent }` on actions, `{ description }` on triggers |
| `classification` | actions **and** triggers | `'READ' \| 'SEARCH' \| 'WRITE' \| 'DESTRUCTIVE'` | **required** — derived from `run()`; triggers are always `'READ'` |

Both are plain values on the `createAction` / `createTrigger` object — no import is needed. Triggers accept `aiMetadata` but **not** `audience`: a trigger is an event, not an agent-callable operation.
All three are plain values on the `createAction` / `createTrigger` object — no import is needed. Triggers accept `aiMetadata` and `classification` but **not** `audience`: a trigger is an event, not an agent-callable operation.

---

Expand All @@ -16,6 +17,7 @@ Both are plain values on the `createAction` / `createTrigger` object — no impo
```typescript
export const createRecordAction = createAction({
name: 'create_record',
classification: 'WRITE',
displayName: 'Create Record',
description: 'Creates a new record in My App', // human-facing
audience: 'both', // 'human' | 'ai' | 'both'
Expand All @@ -41,6 +43,7 @@ export const createRecordAction = createAction({
```typescript
export const createRecordAction = createAction({
name: 'create_record',
classification: 'WRITE',
displayName: 'Create Record',
description: 'Creates a new record in My App',
audience: 'both',
Expand Down Expand Up @@ -81,13 +84,39 @@ Agents use this to reason about safe retries; it maps to the MCP `idempotentHint

---

## `classification` — what the step does to external state

Renders as a badge in the builder's piece selector (`READ`/`SEARCH`/`WRITE` as quiet grey pills, `DESTRUCTIVE` in red) and is the declared read/write signal for future consumers. Convention: place it right after `name:` in the config object.

Classify from the `run()` body and the API call it makes — the name and description are hints, never evidence. **Precedence, first match wins:**

| Value | Means | Typical verbs |
|---|---|---|
| `DESTRUCTIVE` | removes or disables external state; a retry cannot restore it | delete, purge, revoke, cancel, archive, stop/teardown of a subscription or watch |
| `WRITE` | creates or changes external state, recoverably | create, send, post, update, upsert, move, assign, tag |
| `SEARCH` | reads by query or enumeration, zero-or-more results, no mutation | list, search, find, query |
| `READ` | reads a specific known resource or fixed state | get, retrieve, describe, download |

Rules that settle the recurring edge cases:

- **All triggers → `'READ'`**, polling and webhook alike. The badge answers "does this step change anything?"; the READ/SEARCH split is about how you address data (by id vs by query), which is meaningless for an event you did not ask for.
- **Sending is `WRITE`, not `DESTRUCTIVE`** — a sent email/message adds state, it doesn't destroy any.
- **AI/LLM inference or generation** that persists no artifact to an external system → `READ`. "Generate and upload/store" → `WRITE`.
- **Pure in-flow transforms** (text/math/date/json/csv/crypto helpers) → `READ`.
- **Key-value store style pieces**: get → `READ`, put/append → `WRITE`, delete → `DESTRUCTIVE`.
- **One action, multiple operations** (an `operation` prop that can read *or* delete) → tag the worst case reachable.
- **Arbitrary-operation actions** (raw SQL, raw HTTP, caller-supplied method) → `WRITE`. Factory-built actions (`createCustomApiCallAction`) are tagged at the factory level, not per piece.

---

## Triggers

Triggers take `aiMetadata` with `description` only — no `audience`, no `idempotent`:
Triggers take `aiMetadata` with `description` only — no `audience`, no `idempotent` — plus `classification: 'READ'` (always, see above):

```typescript
export const newRecordTrigger = createTrigger({
name: 'new_record',
classification: 'READ',
displayName: 'New Record',
description: 'Triggers when a new record is created',
aiMetadata: {
Expand All @@ -103,12 +132,12 @@ Describe **when the event fires and what one payload represents** (per record? p

## Factory-built actions and triggers

If actions/triggers are produced by a shared factory (a helper that wraps `createAction`/`createTrigger`), the factory's params type must declare `audience`/`aiMetadata` and forward them into the wrapped call — otherwise the fields in your config objects silently fail to compile or never reach the framework. Add the fields to the factory's param type and pass them through.
If actions/triggers are produced by a shared factory (a helper that wraps `createAction`/`createTrigger`), the factory's params type must declare `audience`/`aiMetadata`/`classification` and forward them into the wrapped call — otherwise the fields in your config objects silently fail to compile or never reach the framework. Add the fields to the factory's param type and pass them through.

---

## When to add this

- **New actions and triggers: always.** `audience: 'both'` + `aiMetadata { description, idempotent }` on every action, `aiMetadata { description }` on every trigger.
- **New actions and triggers: always.** `audience: 'both'` + `aiMetadata { description, idempotent }` + `classification` on every action; `aiMetadata { description }` + `classification: 'READ'` on every trigger.
- Set `audience: 'human'` instead when the action is an ask-an-LLM wrapper, a generic transform, or otherwise meaningless as an agent tool.
- Touching an existing untagged action anyway? Tag it while you're there.
2 changes: 1 addition & 1 deletion packages/pieces/community/gmail/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/piece-gmail",
"version": "0.12.10",
"version": "0.13.0",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailAiGetMessageActionOutputSchema } from '../output-schemas';
export const gmailAiGetMessageAction = createAction({
auth: gmailAuth,
name: 'gmail_get_message',
classification: 'READ',
displayName: 'Get Message',
description: 'Get a single email message by its ID.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailAiReplyToThreadActionOutputSchema } from '../output-schemas';
export const gmailAiReplyToThreadAction = createAction({
auth: gmailAuth,
name: 'gmail_reply_to_thread',
classification: 'WRITE',
displayName: 'Reply to Thread',
description: 'Reply to an existing email thread.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailAiSearchEmailActionOutputSchema } from '../output-schemas';
export const gmailAiSearchEmailAction = createAction({
auth: gmailAuth,
name: 'gmail_search_email',
classification: 'SEARCH',
displayName: 'Search Email',
description:
'Search emails using advanced criteria. If no filters are provided, the latest emails are returned.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailAiSendEmailActionOutputSchema } from '../output-schemas';
export const gmailAiSendEmailAction = createAction({
auth: gmailAuth,
name: 'gmail_send_email',
classification: 'WRITE',
displayName: 'Send Email',
description: 'Send an email through a Gmail account',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { gmailCreateDraftActionOutputSchema } from '../output-schemas';
export const gmailCreateDraftAction = createAction({
auth: gmailAuth,
name: 'gmail_create_draft',
classification: 'WRITE',
displayName: 'Create Draft',
description:
'Create a new draft email (optionally inside an existing thread).',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createDraftReplyActionOutputSchema } from '../output-schemas';
export const gmailCreateDraftReplyAction = createAction({
auth: gmailAuth,
name: 'create_draft_reply',
classification: 'WRITE',
description: 'Creates a draft reply to an existing email.',
audience: 'human',
aiMetadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { gmail as googleGmail } from '@googleapis/gmail';
export const gmailDeleteDraftAction = createAction({
auth: gmailAuth,
name: 'gmail_delete_draft',
classification: 'DESTRUCTIVE',
displayName: 'Delete Draft',
description: 'Permanently delete a draft email by its ID.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { gmailForwardMessageActionOutputSchema } from '../output-schemas';
export const gmailForwardMessageAction = createAction({
auth: gmailAuth,
name: 'gmail_forward_message',
classification: 'WRITE',
displayName: 'Forward Message',
description: 'Forward an existing email to new recipients.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailGetAttachmentActionOutputSchema } from '../output-schemas';
export const gmailGetAttachmentAction = createAction({
auth: gmailAuth,
name: 'gmail_get_attachment',
classification: 'READ',
displayName: 'Get Attachment',
description: 'Download an email attachment by message ID and attachment ID.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailGetDraftActionOutputSchema } from '../output-schemas';
export const gmailGetDraftAction = createAction({
auth: gmailAuth,
name: 'gmail_get_draft',
classification: 'READ',
displayName: 'Get Draft',
description: 'Get a single draft email by its ID.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailGetLabelActionOutputSchema } from '../output-schemas';
export const gmailGetLabelAction = createAction({
auth: gmailAuth,
name: 'gmail_get_label',
classification: 'READ',
displayName: 'Get Label',
description:
'Get a single label by its ID, including message and thread counts.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { gmailGetMailActionOutputSchema } from '../output-schemas';
export const gmailGetEmailAction = createAction({
auth: gmailAuth,
name: 'gmail_get_mail',
classification: 'READ',
description: 'Get an email via Id.',
audience: 'human',
aiMetadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailGetProfileActionOutputSchema } from '../output-schemas';
export const gmailGetProfileAction = createAction({
auth: gmailAuth,
name: 'gmail_get_profile',
classification: 'READ',
displayName: 'Get Profile',
description: 'Get the connected mailbox profile.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { gmailGetThreadActionOutputSchema } from '../output-schemas';
export const gmailGetThread = createAction({
auth: gmailAuth,
name: 'gmail_get_thread',
classification: 'READ',
description: 'Get a thread from your Gmail account via Id',
displayName: 'Get Thread',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailListDraftsActionOutputSchema } from '../output-schemas';
export const gmailListDraftsAction = createAction({
auth: gmailAuth,
name: 'gmail_list_drafts',
classification: 'SEARCH',
displayName: 'List Drafts',
description: 'List draft emails in the mailbox.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailListHistoryActionOutputSchema } from '../output-schemas';
export const gmailListHistoryAction = createAction({
auth: gmailAuth,
name: 'gmail_list_history',
classification: 'SEARCH',
displayName: 'List History',
description: 'List mailbox changes since a given history ID.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailListLabelsActionOutputSchema } from '../output-schemas';
export const gmailListLabelsAction = createAction({
auth: gmailAuth,
name: 'gmail_list_labels',
classification: 'SEARCH',
displayName: 'List Labels',
description: 'List all labels in the mailbox.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailListThreadsActionOutputSchema } from '../output-schemas';
export const gmailListThreadsAction = createAction({
auth: gmailAuth,
name: 'gmail_list_threads',
classification: 'SEARCH',
displayName: 'List Threads',
description: 'List email conversation threads in the mailbox.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { replyToEmailActionOutputSchema } from '../output-schemas';
export const gmailReplyToEmailAction = createAction({
auth: gmailAuth,
name: 'reply_to_email',
classification: 'WRITE',
displayName: 'Reply to Email',
description: 'Reply to an existing email.',
audience: 'human',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { requestApprovalInMailActionOutputSchema } from '../output-schemas';
export const requestApprovalInEmail = createAction({
auth: gmailAuth,
name: 'request_approval_in_mail',
classification: 'WRITE',
displayName: 'Request Approval in Email',
description:
'Send approval request email and then wait until the email is approved or disapproved',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { gmailSearchMailActionOutputSchema } from '../output-schemas';
export const gmailSearchMailAction = createAction({
auth: gmailAuth,
name: 'gmail_search_mail',
classification: 'SEARCH',
displayName: 'Find Email',
description:
'Find emails using advanced search criteria. If no filters are provided, the latest emails are returned.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { gmailSendDraftActionOutputSchema } from '../output-schemas';
export const gmailSendDraftAction = createAction({
auth: gmailAuth,
name: 'gmail_send_draft',
classification: 'WRITE',
displayName: 'Send Draft',
description: 'Send an existing draft email by its ID.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { sendEmailActionOutputSchema } from '../output-schemas';
export const gmailSendEmailAction = createAction({
auth: gmailAuth,
name: 'send_email',
classification: 'WRITE',
description: 'Send an email through a Gmail account',
audience: 'human',
aiMetadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { gmail as googleGmail } from '@googleapis/gmail';
export const gmailStopWatchAction = createAction({
auth: gmailAuth,
name: 'gmail_stop_watch',
classification: 'DESTRUCTIVE',
displayName: 'Stop Watch',
description: 'Stop push notifications on the mailbox.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AddressObject } from 'mailparser';
export const gmailUpdateDraftAction = createAction({
auth: gmailAuth,
name: 'gmail_update_draft',
classification: 'WRITE',
displayName: 'Update Draft',
description: 'Replace the content of an existing draft email.',
audience: 'ai',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Props = {
export const gmailNewAttachmentTrigger = createTrigger({
auth: gmailAuth,
name: 'new_attachment',
classification: 'READ',
displayName: 'New Attachment',
description: 'Triggers when an email with an attachment arrives.',
aiMetadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function extractParticipants(messages: any[]): {
export const gmailNewConversationTrigger = createTrigger({
auth: gmailAuth,
name: 'new_conversation',
classification: 'READ',
displayName: 'New Conversation',
description: 'Triggers when a new email conversation (thread) begins',
props: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { gmailNewEmailReceivedTriggerOutputSchema } from '../output-schemas';
export const gmailNewEmailTrigger = createTrigger({
auth: gmailAuth,
name: 'gmail_new_email_received',
classification: 'READ',
displayName: 'New Email',
description: 'Triggers when new mail is found in your Gmail inbox',
aiMetadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const TRIGGER_KEY = 'labels';
export const gmailNewLabelTrigger = createTrigger({
auth: gmailAuth,
name: 'new_label',
classification: 'READ',
displayName: 'New Label',
description: 'Triggers when a new label is created.',
aiMetadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async function enrichGmailMessage({
export const gmailNewLabeledEmailTrigger = createTrigger({
auth: gmailAuth,
name: 'new_labeled_email',
classification: 'READ',
displayName: 'New Labeled Email',
description: 'Triggers when a label is added to an email',
aiMetadata: {
Expand Down
Loading