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
5 changes: 4 additions & 1 deletion .agents/skills/piece-builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ The condensed rules in this file (Quick Auth Reference, Quick Piece Definition T
| A connection needs a human-readable label in the UI (account email, workspace name) | `auth-patterns.md` (Connection Identifier) |
| Your first action in this piece (full file shape) | `action-patterns.md` |
| A trigger — polling, webhook, handshake, or renewal | `trigger-patterns.md` |
| A prop type you haven't used (dropdowns, dynamic, arrays, files) | `props-patterns.md` |
| **Choosing which prop component, display mode, or layout/grouping fits a use case** | `property-ui-selection.md` |
| The exact syntax of a prop type (dropdowns, dynamic, arrays, files) | `props-patterns.md` |
| 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` |
Expand Down Expand Up @@ -192,6 +193,8 @@ export const myApp = createPiece({

Pieces are used by people who have never seen an API — props, dropdowns, and descriptions must be self-explanatory.

**Before defining any `props`, read `property-ui-selection.md`** to pick the right component, display mode (`cards` / `stepper` / rich-text / date-range), and layout/grouping (`tabs` / `section` / filter `builder`) for each field.

1. **Never ask users to type IDs** — Use dynamic dropdowns so they pick items by name (`"Jane Doe (jane@x.com)"` not `"cus_abc123"`).
2. **Descriptions must teach** — Don't say "Enter the thread timestamp." Say "Click the three dots next to the message, select Copy Link, and paste the number at the end."
3. **Use Markdown instructions** for complex setup — Add `Property.MarkDown()` with numbered steps when a prop requires configuration in the third-party app.
Expand Down
183 changes: 183 additions & 0 deletions .agents/skills/piece-builder/property-ui-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Property UI Selection Guide

**Read this before defining `props` on any action or trigger.** `props-patterns.md` tells you the *syntax* of each property type; this file tells you *which component, display mode, and layout to pick for the use case* so the step form reads well for a non-technical user.

The mental model: **choose the input type → apply a display upgrade if one fits → arrange with layout hints → group with `propertyGroups` only when the form is large or has distinct modes.** Most props need only the first step.

---

## 1. Pick the input component (by use case)

| The user needs to enter… | Use | Notes |
|---|---|---|
| A short single-line value (name, email, id) | `Property.ShortText` | Add `placeholder`. Prefer a dropdown over a raw ID field — see rule below. |
| A long free-form value (notes, description) | `Property.LongText` | Multi-line textarea. |
| A formatted message body (email, chat post) | `Property.RichText` + `formatProperty` | Toolbar + `{{ variables }}`. Pair with a sibling format dropdown. See §2. |
| Yes/no, on/off | `Property.Checkbox` | Use `reveals` to show dependent fields only when on. See §2. |
| A number | `Property.Number` | Add `display: 'stepper'` for bounded counts. See §2. |
| One choice from a **fixed** list | `Property.StaticDropdown` | Add `display: 'cards'` for ≤4 visual choices. See §2. |
| Many choices from a fixed list | `Property.StaticMultiSelectDropdown` | |
| One choice **fetched from the API** | `Property.Dropdown` | `refreshers`, `options` async. This is the answer to "don't make users type IDs". |
| Many choices fetched from the API | `Property.MultiSelectDropdown` | |
| A date + time | `Property.DateTime` | Single instant. |
| A time **window** (last 7 days, custom range) | `Property.DateRange` | For search/filter actions. `display: 'dropdown'` inside a filter builder. See §2. |
| Fields that change based on earlier input/API | `Property.DynamicProperties` | Build the sub-form in `props()`. Heaviest option — use only when the shape is genuinely runtime-dependent. |
| A file (upload or URL) | `Property.File` | Returns `ApFile`. |
| A color | `Property.Color` | Swatch + hex. |
| Raw JSON | `Property.Json` | Only when there is no better structured option. |
| Free-form key→value pairs | `Property.Object` | Dictionary editor. |
| A list of plain strings | `Property.Array` (no `properties`) | Tags, emails. |
| A list of structured rows | `Property.Array` **with** `properties` | Repeating record editor (e.g. line items). |
| Read-only instructions / setup steps | `Property.MarkDown` | Display-only, collects nothing. Use `variant` (INFO/WARNING/TIP/BORDERLESS). |
| A fully custom widget (embedding only) | `Property.Custom` (BETA) | DOM injection. Requires `minimumSupportedRelease >= 0.58.0`. Avoid unless embedding. |

**Never make users type an opaque ID.** If a value exists behind the API (channel, project, contact, board), use `Property.Dropdown` so they pick it by name. Raw `ShortText` for an ID is a last resort and needs a `description` that explains exactly where to find it.

---

## 2. Display upgrades (opt-in, per property)

These are optional `display`/pairing hints on an otherwise normal property. They are safe to add and ignored where they don't apply.

### `StaticDropdown` → `display: 'cards'`
Use for a **small set (2–4) of mutually exclusive modes** where each choice benefits from an icon + one-line explanation (e.g. Plain text / HTML / Markdown). Do **not** use cards for long lists — they don't scroll well.
```typescript
Property.StaticDropdown({
displayName: 'Format', required: true, defaultValue: 'plain_text', display: 'cards',
options: { options: [
{ label: 'Plain text', value: 'plain_text', description: 'Simple', icon: 'text' },
{ label: 'HTML', value: 'html', description: 'Rich + styled', icon: 'code' },
] },
});
```

### `Number` → `display: 'stepper'`
Use for a **bounded** count the user nudges (max results, retries, quantity). Requires sensible `min`/`max`; add `step`.
```typescript
Property.Number({ displayName: 'Max results', required: false, defaultValue: 10, display: 'stepper', min: 1, max: 500, step: 1 });
```

### `RichText` + `formatProperty`
Use for any **message body** the user composes. Pair it with a sibling `StaticDropdown` (ideally `display: 'cards'`) and point `formatProperty` at that dropdown's **name**. The returned value is a plain string in the chosen format.
- Sibling value mapping (by convention): `plain_text` / `plain` / `text` → plain · `html` → HTML · `markdown` / `md` → markdown · anything else → plain.

### `DateRange` (+ `display: 'dropdown'`)
Use for "limit results to a time window" on search/list actions. Omit `display` for pill buttons; set `display: 'dropdown'` when it lives inside a filter builder. Resolve in `run()`:
```typescript
import { dateRangeUtils } from '@activepieces/pieces-framework';
const { after, before } = dateRangeUtils.resolve(context.propsValue.date_range); // ISO strings or undefined
```

### `Checkbox` → `reveals`
Use to **progressively disclose** fields that only matter when the toggle is on. List the dependent prop names; they render indented beneath the toggle.
```typescript
has_attachment: Property.Checkbox({ displayName: 'Has attachment', required: false, defaultValue: false, reveals: ['attachment_name'] }),
attachment_name: Property.ShortText({ displayName: 'Attachment name', required: false, placeholder: 'e.g. invoice.pdf' }),
```

---

## 3. Layout hints (any property)

These live directly on the property. They fine-tune placement without changing the input.

| Hint | Value | Use when |
|---|---|---|
| `placeholder` | string | Any text input — show an example value (`you@example.com`). |
| `width` | `'half'` | Two short related fields should sit side-by-side (First / Last name). Only takes effect **inside a `section` group**. |
| `icon` | icon name | Give a filter-builder row or section field a leading glyph. Must be a **valid name** — see §5. |
| `advanced` | `true` / `false` | `false` promotes a normally-optional field into the main form (e.g. a message body). `true` pushes an important-looking field into the collapsible **Advanced** section. |

**Advanced section rule:** non-required props collapse into *Advanced* by default. Reach for `advanced: false` when an optional field is actually central to the action.

---

## 4. Grouping props with `propertyGroups`

Only add groups when the form is large or has distinct concerns. A short form (≤4 fields) needs none. Declare `propertyGroups` on the action/trigger; each group lists members by `name`:

```typescript
propertyGroups: [{ key, display, label?, description?, icon?, props: ['fieldA', 'fieldB'] }]
```
(Threaded through both `createAction` and `createTrigger`.)

**Pick the layout by intent:**

| Intent | `display` | Behaviour |
|---|---|---|
| Mutually-exclusive **modes** of the same concept (To / Cc / Bcc; by-URL vs by-ID) | `'tabs'` | Segmented control; one tab's fields visible at a time. |
| **Related fields as a titled card** (a "Send to" card, a "Message" card) | `'section'` | Titled card; `width: 'half'` packs two-up. **Keeps the Advanced section** — ungrouped optional props still collapse as usual. |
| A **search/filter** action where users add only the filters they need | `'builder'` | Progressive "Add filter" picker; each `builder` group is a category. A filter row persists only when its value is set — give each filter a `placeholder` + `icon`. |
| A pinned control **below** a filter builder (result limit) | `'footer'` | Pins its prop (e.g. a `stepper`) under the builder list. Pair with `'builder'` groups. |

**Rules:**
- Every prop named in a group must exist in `props`.
- Props left out of every group follow the normal essential/Advanced rule (only `section` preserves this — `tabs` and `builder` take full control of their members).
- Give `section` and `builder` groups a `label` and `icon` so cards/categories read clearly.

---

## 5. Valid `icon` names

`icon` accepts only these keys (each maps to a Lucide icon). Any other string renders nothing:

```
text · code · markdown · reply · reply-all · users · user · send · type
file · paperclip · tag · inbox · calendar · trash · filter · sliders · blank
```

---

## 6. Worked examples

**Send a chat message — sectioned cards + card dropdown + rich body:**
```typescript
propertyGroups: [
{ key: 'destination', display: 'section', label: 'Send to', icon: 'send', props: ['chat_id'] },
{ key: 'message', display: 'section', label: 'Message', icon: 'text', props: ['format', 'message'] },
],
props: {
chat_id: Property.ShortText({ displayName: 'Chat Id', required: true, placeholder: '@channel or 123456789' }),
format: Property.StaticDropdown({ displayName: 'Format', required: false, display: 'cards', options: { options: [/* Markdown / HTML / Plain */] } }),
message: Property.RichText({ displayName: 'Message', required: true, formatProperty: 'format' }),
disable_notification: Property.Checkbox({ displayName: 'Disable notification', required: false }), // → Advanced
},
```

**Search emails — filter builder + footer stepper + date range:**
```typescript
propertyGroups: [
{ key: 'people', display: 'builder', label: 'People', icon: 'users', props: ['from', 'to'] },
{ key: 'time', display: 'builder', label: 'Time', icon: 'calendar', props: ['date_range'] },
{ key: 'footer', display: 'footer', props: ['max_results'] },
],
props: {
from: Property.ShortText({ displayName: 'From', required: false, icon: 'user', placeholder: 'sender@example.com' }),
to: Property.ShortText({ displayName: 'To', required: false, icon: 'send', placeholder: 'recipient@example.com' }),
date_range: Property.DateRange({ displayName: 'Date', required: false, display: 'dropdown', icon: 'calendar' }),
max_results: Property.Number({ displayName: 'Max results', required: false, defaultValue: 10, display: 'stepper', min: 1, max: 500 }),
},
```

**Recipients — segmented tabs:**
```typescript
propertyGroups: [{ key: 'recipients', display: 'tabs', label: 'Recipients', props: ['to', 'cc', 'bcc'] }],
props: {
to: Property.Array({ displayName: 'To', required: true }),
cc: Property.Array({ displayName: 'Cc', required: false }),
bcc: Property.Array({ displayName: 'Bcc', required: false }),
},
```

---

## 7. Anti-patterns

- **Raw ID `ShortText` where a `Dropdown` is possible.** Pick-by-name beats copy-paste-an-id every time.
- **`display: 'cards'` on a long list.** Cards are for 2–4 modes; use a plain dropdown otherwise.
- **`propertyGroups` on a 3-field form.** Grouping is overhead — only add it for large or multi-mode forms.
- **Invalid `icon` name.** Anything outside the §5 list silently renders nothing; verify before shipping.
- **`Property.Json` as an escape hatch.** If the shape is known, model it with real props or an `Array` of fields.
- **`Property.DynamicProperties` for a static form.** It's the heaviest widget; only use it when fields truly depend on runtime data.

Full type syntax and dynamic-dropdown/refresher mechanics: `props-patterns.md`. Rendered previews of every option: `docs/build-pieces/piece-reference/properties.mdx`.
1 change: 1 addition & 0 deletions brain/knowledge/flows-execution/flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Flows are the core automation primitive: a versioned directed graph of trigger +
- **Draft creation is now atomic, in two different ways — know which one you're in.** `createNewDraftIfVersionIsPublished` runs `createEmptyVersion` + the IMPORT_FLOW loop in one `transaction()`, threading the `entityManager` down through `applyOperation` to the `updateLastModified` side effect. The *user's* operation deliberately stays outside that transaction (it would hold Postgres open across `prepareRequest` piece-metadata fetches and non-rollbackable file/webhook side effects) and is instead undone by a compensating `delete` of the freshly created draft. `flowService.create` wraps the flow row + first empty version the same way, so a failure can't leave a zero-version, unopenable flow.
- **`flowVersionSideEffects.preApplyOperation` writes on the default connection, so it escapes any caller transaction.** `handleSampleDataDeletion` and `handleUpdateTriggerWebhookSimulation` take no `entityManager`; a write they make from inside a `transaction()` survives its rollback. They early-return for `IMPORT_FLOW`/`UPDATE_SAMPLE_DATA_INFO`, so the transactional path above is safe today — but adding an operation type to it silently reintroduces partial commits. `updateLastModified` sits outside that swallow-all catch on purpose: a swallowed statement failure inside a transaction poisons it and resurfaces as a confusing "transaction is aborted" error on the next statement.
- **`transaction()` (`core/db/transaction.ts`) is a bare `dataSource.transaction()`** — it acquires a *new* connection, not a savepoint. Nesting it deadlocks, so check every caller before wrapping a service method that others may already call inside a transaction.
- Step settings split a piece's props into an always-visible **essential** set and a collapsed **Advanced** section: a prop is Advanced only when it sets `advanced: true` (everything else — incl. `MARKDOWN`, tab/section group members, and checkbox reveal targets — stays essential). `propertyGroups` render as tabs, sectioned cards, or the "Add filter" builder.
- **Flows stuck in `DELETING` keep eating the active-flow limit.** Deletion is a durable BullMQ system job (`delete-flow-<flowId>`), not synchronous: `delete()` sets `operationStatus=DELETING` and enqueues, and the row plus `status=ENABLED` only go away when the job finishes. That job runs `sampleDataService.deleteForFlow`, whose `DELETE FROM file … metadata->>'flowId'=?` had no index — on the large prod `file` table it seq-scans, blows `statement_timeout`, exhausts its 2 attempts and lands **permanently** in the failed set. The flow is then hidden from the UI list (which filters `!=DELETING`) but still counted by the active-flows quota (`getUsage` counts `status=ENABLED`), so Publish silently shows the "Purchase Extra Active Flows" dialog instead of publishing — this is what breaks the `webhook-should-return-response` e2e monitor. Stuck flows are functionally dead (`preDelete` disables the trigger before the failing delete), so forcing their rows away is safe. Fixes on `fix/flow-delete-sample-data-timeout`: a partial expression index `idx_file_sample_data_flow_id` on `file (type, (metadata->>'flowId'))`, plus `operationStatus != DELETING` in the active-flow counts so the quota stops depending on delete-job success.

### Editions
Expand Down
2 changes: 1 addition & 1 deletion brain/knowledge/flows-execution/formulas.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ In-builder data transformation: users transform any text input using ~104 functi

### Entities & files
- `core/shared/src/lib/formula/` — `formula-evaluator.ts`, `function-registry.ts` (`AP_FUNCTIONS`, the single source of truth), `function-implementations.ts`, `function-type-checker.ts`.
- Editor: `web/.../text-input-with-mentions/tiptap-editor.tsx` (always registers `FunctionSlashExtension` + the three inline atom badge nodes — no plan flag), search/hover popovers, `text-input-utils.ts` (doc ⇄ wrapped-string serializer).
- Editor: `web/.../text-input-with-mentions/tiptap-editor.tsx` (always registers `FunctionSlashExtension` + the three inline atom badge nodes — no plan flag), search/hover popovers, `text-input-utils.ts` (doc ⇄ wrapped-string serializer). An `outputFormat: 'text' | 'html'` prop (default `'text'`) switches it into a rich-text WYSIWYG (StarterKit + toolbar) that serializes to/from HTML with mentions kept as `{{...}}` tokens; consumed by the `RICH_TEXT` property widget.

### Gotchas
- On **every** edition, unconditionally on — no plan flag or license toggle. The pre-pass runs regardless of any editor flag, so saved formulas keep evaluating even where the editor is off. Only embed difference: the search popover hides the external "See All" docs link.
Expand Down
5 changes: 4 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading