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
14 changes: 8 additions & 6 deletions .agents/skills/piece-builder/property-ui-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ These live directly on the property. They fine-tune placement without changing t
| `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` | `true` | Tucks a secondary field into the collapsible **Advanced** section. Nothing collapses there unless you set it — `advanced: false` is the default and does nothing. |

**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.
**Advanced section rule:** every prop renders in the main form by default, required or not. Opt a field *out* with `advanced: true`. Don't set it on a required prop — the Advanced section starts collapsed, so a mandatory field hidden there only surfaces as a validation error.

---

Expand All @@ -106,13 +106,14 @@ propertyGroups: [{ key, display, label?, description?, icon?, props: ['fieldA',
| 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. |
| **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** for props outside the cards — group members are always essential. |
| 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. |
| 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 — like `'builder'`, it disables the Advanced section form-wide. |

**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).
- Only ungrouped props honour `advanced: true` — members of `tabs` and `section` groups are always essential; the flag is ignored on them. In a sectioned layout, checkbox `reveals` targets are forced essential too: they render inline under their toggle, never in Advanced.
- **One `builder` or `footer` group disables the Advanced section for the whole action/trigger** — every prop is forced essential and `advanced: true` stops working form-wide. Don't combine a filter builder with Advanced props.
- Give `section` and `builder` groups a `label` and `icon` so cards/categories read clearly.

---
Expand Down Expand Up @@ -140,7 +141,7 @@ 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
disable_notification: Property.Checkbox({ displayName: 'Disable notification', required: false, advanced: true }), // → Advanced (ungrouped + flagged)
},
```

Expand Down Expand Up @@ -179,5 +180,6 @@ props: {
- **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.
- **`advanced: true` on a required prop.** Advanced starts collapsed; a mandatory field hidden there only surfaces as a validation error.

Full type syntax and dynamic-dropdown/refresher mechanics: `props-patterns.md`. Rendered previews of every option: `docs/build-pieces/piece-reference/properties.mdx`.
2 changes: 2 additions & 0 deletions brain/knowledge/connections-auth/ce-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ The core (all-editions) auth layer: user identity creation, sign-in, and JWT ses
- **A new unauthenticated endpoint must be added to `disallowedRoutes` in `packages/web/src/lib/api.ts`**, otherwise the SPA attaches whatever stale bearer token is still in storage and the call fails in exactly the situation the endpoint exists for.
- **The three signup guards in `authentication-utils.ts` differ in what they leak.** `assertEmailAuthIsEnabled` and `assertDomainIsAllowed` describe platform configuration, so surfacing their errors is safe. `assertUserIsInvitedToPlatformOrProject` describes one address, so surfacing it turns any public auth endpoint into an invitation oracle. All three are also inert unless `plan.ssoEnabled`.
- **A nil `projectId` on the principal means "go to /create-platform" in four separate places.** Anything that mints a platform-less session has to satisfy all of them, not just the route guard.
- **The route no longer decides sign-in vs sign-up — the card does.** `/sign-in`, `/sign-up` and `/create-platform` all render the same `AuthLanding`; `/sign-up` is a bare redirect to `/sign-in`. Which form you get is a function of two flags: with `SMTP_CONFIGURED` the card opens on the email-code step and the classic password form exists *only* behind the "Use password" link; without it you land on a password form directly, and `USER_CREATED` picks sign-up (first ever account, no mode switch offered) over sign-in. So the same URL renders three different DOMs across Cloud, a seeded self-host, and a fresh install — anything scripting this screen has to branch, and password sign-*up* is simply unreachable once SMTP is on.
- **`/create-platform` is that same card opening on its name step**, off the ONBOARDING token rather than a route param — submitting the name is what mints the platform and project and swaps ONBOARDING for USER. A brand-new account therefore needs *two* form submissions before it has a project, which is easy to miss when automating first-run signup.

### Key files
Entry point: `authenticationService`, a log-taking factory called per request from `authentication.controller.ts`, registered as `authenticationModule` in `app.ts`.
Expand Down
28 changes: 28 additions & 0 deletions brain/knowledge/engineering/e2e-tests-and-monitors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
icon: 🎭
---

# E2E Tests & Monitors

One Playwright suite in `packages/tests-e2e` feeds three consumers that fail independently: CI on a fresh throwaway instance, Checkly monitors against **production Cloud**, and a single BetterStack monitor. A change that only breaks one of them looks green everywhere else, so it is worth knowing which reads what.

**Local / CI suite** — `playwright.config.ts`, `testMatch: **/*.spec.ts`, split by `AP_EDITION` into `scenarios/ce` and `scenarios/ee`. Boots the whole stack itself via the `webServer` block.
**Checkly monitors** — `checkly.config.ts` picks up the *same* `**/scenarios/**/*.spec.ts` files and runs them every 10 minutes with `baseURL: https://cloud.activepieces.com`, signing in with `E2E_EMAIL` / `E2E_PASSWORD`.
**BetterStack monitor** — one standalone file, `scenarios/betterstack/*.flat.spec.js`.

## Gotchas

- **Checkly runs the shared specs against production Cloud**, so anything the page objects assume about the login screen has to hold on Cloud too — not just on the SMTP-less instance CI boots. This is the usual reason a UI change breaks the monitors but not CI. See the auth-card gotchas on [CE Authentication](../connections-auth/ce-authentication.md).
- **BetterStack does not read the repo — the repo pushes to it.** `.github/workflows/sync-betterstack-playwright.yml` fires on push to `main` and `PATCH`es the file's contents into the hardcoded monitor `4211060` as `playwright_script`. One-way and `main`-only: the monitor updates at *merge*, never on the PR, and any edit made in the BetterStack UI is silently overwritten by the next push.
- **The BetterStack file is deliberately flat and duplicated.** BetterStack executes one self-contained script, so it cannot `require` the `pages/` objects — its sign-in is a copy. Fix the page object and you have *not* fixed the monitor; both files need the change.
- **The `.flat.spec.js` runs nowhere else.** Playwright matches `*.spec.ts` and Checkly matches `**/scenarios/**/*.spec.ts`, so a broken flat file is invisible locally and in CI until it fails in BetterStack.
- **CI only runs on the `ready-for-e2e` label** (`e2e.yml` gates both edition workflows on it), which is why the suite can rot for weeks without anyone noticing.
- **Turbo strict env mode silently strips most of `.env.e2e`.** `globalPassThroughEnv` in `turbo.json` is an allow-list, so vars not named there never reach the `serve` tasks — verify with `tr '\0' '\n' < /proc/<api-pid>/environ`. `AP_ENVIRONMENT` is among the casualties, so CI falls back to the `prod` default. Widening it to `AP_*` does forward them, but that alone broke worker→API Socket.IO auth (jobs queue up unconsumed), so the passthrough and the worker's `AP_WORKER_TOKEN` have to be sorted out together.
- **`AP_DEV_PIECES` loads from `packages/pieces/**/dist`, which `npm run dev` does not build.** Only pieces that happen to be build dependencies of api/worker have a `dist`, so a default dev instance serves **0 pieces** and every spec that picks a trigger times out on the piece search. Build them explicitly: `npx turbo run build --filter=@activepieces/piece-webhook --filter=@activepieces/piece-store`.
- **CE sign-up is invitation-only once a platform exists** (`INVITATION_ONLY_SIGN_UP`), so the suite's sign-up path only works on a genuinely fresh instance. Against a dev-seeded database, set `E2E_EMAIL` / `E2E_PASSWORD` instead — `global-setup.ts` prefers them and signs in rather than signing up.
- **Every workspace declares its own deps.** `@faker-js/faker` was imported by the page objects for months while only `server/api` declared it; under Bun's isolated linker that means the suite cannot import its own page objects at all.

## Key files
- `packages/tests-e2e` — `playwright.config.ts` (local/CI), `checkly.config.ts` (Cloud monitors), `global-setup.ts` (provisions or signs in the seed account), `pages/` (shared page objects), `scenarios/betterstack/` (the standalone monitor script)
- `.github/workflows/e2e.yml` — the `ready-for-e2e` gate that calls the per-edition workflows
- `.github/workflows/sync-betterstack-playwright.yml` — the push-to-`main` upload
1 change: 1 addition & 0 deletions brain/knowledge/engineering/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ The **Activepieces engineering brain**: how the system works, and *why* it was b
- **Web Feature Anatomy** — the frontend feature folder, its barrel, route guards, and when a query gets the global error dialog
- **Cloud Deployment Paths** — canary → prod, the `cloud-hotfix` override, and the breaking-migration gate that blocks both
- **CI PR Review Hygiene** — draft-first Greptile review, the per-area PR size gate, and the workflow conventions reviewers keep re-litigating
- **E2E Tests & Monitors** — the one Playwright suite behind CI, the Checkly monitors that run it against production Cloud, and the BetterStack script the repo pushes on merge
- **Architecture Spine** — the load-bearing structure of the codebase, and the gotchas that come with it: request-body `.max()` as data loss, TypeORM soft-delete across a canary window, and canary not proxying websockets
1 change: 1 addition & 0 deletions bun.lock

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

12 changes: 6 additions & 6 deletions docs/build-pieces/piece-reference/properties.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ Every property accepts a few optional hints that fine-tune how it renders. They
| `placeholder` | text inputs | Grey hint text shown inside an empty field (e.g. `you@example.com`). |
| `width: 'half'` | any prop inside a group | Renders two fields side-by-side instead of full-width. |
| `icon` | any prop | A named icon shown beside the field in the filter builder. |
| `advanced: false` | non-required props | Forces a normally-optional field to stay **outside** the collapsible *Advanced* section. |
| `advanced: true` | any prop | Moves the field into the collapsible *Advanced* section. Props render in the main form by default. |

Non-required properties are collapsed into an **Advanced** section by default. Set `advanced: false` to promote an important optional field (like a message body) back into the main form, or `advanced: true` to push a field into Advanced.
Every property renders in the main form by default, required or not. Set `advanced: true` on a secondary option to tuck it into the collapsible **Advanced** section `advanced: false` is the default and has no effect. Avoid the flag on required props: the section starts collapsed, so a mandatory field hidden there only surfaces as a validation error.

**Half-width fields**

Expand Down Expand Up @@ -606,12 +606,12 @@ createAction({
```

<Tip>
A filter row is shown when its value is set, so there's nothing extra to persist. Give filters short `placeholder` hints and an `icon` so each row reads clearly.
A filter row is shown when its value is set, so there's nothing extra to persist. Give filters short `placeholder` hints and an `icon` so each row reads clearly. Note that a `builder` or `footer` group also switches off the *Advanced* section for the whole step — every prop lives in the builder.
</Tip>

### Sectioned cards

`display: 'section'` groups related props into titled cards — for example a *Send to* card and a *Message* card. Unlike tabs and the filter builder, sectioned layouts **keep the collapsible _Advanced_ section**: any prop you don't place in a section still follows the normal essential/advanced rule, so secondary options stay tucked away. Give each group a `label` and `icon`, and use `width: 'half'` on members to pack two fields per row.
`display: 'section'` groups related props into titled cards — for example a *Send to* card and a *Message* card. Unlike tabs and the filter builder, sectioned layouts **keep the collapsible _Advanced_ section** for props outside the cards: an ungrouped prop still honours `advanced: true` — unless it is a checkbox `reveals` target, which renders inline under its toggle instead. Props inside a section are always essential. Give each group a `label` and `icon`, and use `width: 'half'` on members to pack two fields per row.

<SectionCardsPreview />

Expand All @@ -626,8 +626,8 @@ createAction({
chat_id: Property.ShortText({ displayName: 'Chat Id', required: true, placeholder: '@channelusername 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' }),
// props left out of every group collapse into Advanced as usual
disable_notification: Property.Checkbox({ displayName: 'Disable notification', required: false }),
// ungrouped props can opt into Advanced with advanced: true
disable_notification: Property.Checkbox({ displayName: 'Disable notification', required: false, advanced: true }),
},
});
```
Expand Down
24 changes: 24 additions & 0 deletions docs/install/reference/breaking-changes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ This affects the Tables piece's Find Records action and any direct API call that

Nothing on upgrade. Re-check any flow or API integration that filters a Date column with `gt`, `gte`, `lt` or `lte` — it now returns the rows the filter actually describes, which may be more or fewer than before.

#### Emailed sign-in codes allow ten wrong guesses per account per hour

The six-digit login code already allowed five wrong guesses, but that budget lived on the code itself, and the fifth wrong guess threw the code away — so asking for a new code handed out five fresh guesses immediately, with no ceiling on how often that could repeat. A six-digit code is only a million possibilities, so unlimited retries reduce it to a matter of hours.

Wrong guesses are now counted per account over a rolling hour, independently of how many codes get sent. Entering the right code clears the counter, so someone who fumbles a few digits and then succeeds is unaffected.

Two consequences worth knowing. Someone who spends ten wrong guesses on an account within an hour cannot sign in **with an emailed code** until the hour is up; password and Google sign-in are unaffected. And because the counter is keyed on the account rather than the caller, anyone who knows an address can spend that budget on the owner's behalf — a temporary nuisance for the owner, and the trade the cap is worth making.

#### What you need to do

Nothing. The limit applies out of the box and needs no configuration. If your support team sees a report of "the code keeps saying it's wrong", have them check whether the account has burned its hourly budget, and point the user at password or Google sign-in in the meantime.

#### One-time codes are no longer stored in a readable form

The `otp` table used to hold the six-digit sign-in code as plain text, so anyone who could read the database — a replica, a backup, a support query — could sign in as any account for the ten minutes that code was alive, without a password. Codes are now stored as a digest keyed with a server-held secret, so reading the table no longer yields anything you can sign in with. The code itself is held only long enough to re-send it if the person asks for it again.

A code found to be expired is thrown away when it is next presented, rather than sitting in the table until the same person happens to request another one.

#### What you need to do

Nothing, and no new configuration: the key is derived from a secret your instance already has. Codes written by an older build are recorded as such and keep working until they expire, so a sign-in already underway when you deploy still completes, an email verification link still opens, and a rolling deploy where both builds are serving at once behaves the same.

Rolling back costs at most the codes issued after the deploy: the older build cannot read those, so whoever holds one asks for a fresh code. Nothing is rewritten and nothing is deleted, so no cleanup is needed either way.


## 0.87.0

Expand Down
2 changes: 1 addition & 1 deletion packages/core/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/shared",
"version": "0.137.0",
"version": "0.138.0",
"type": "commonjs",
"sideEffects": false,
"main": "./dist/src/index.js",
Expand Down
1 change: 1 addition & 0 deletions packages/core/shared/src/lib/ee/otp/otp-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const OtpModel = z.object({
value: z.string(),
state: z.nativeEnum(OtpState),
attempts: z.number(),
version: z.number(),
})

export type OtpModel = z.infer<typeof OtpModel>
7 changes: 6 additions & 1 deletion packages/pieces/framework/src/lib/piece-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ export const AiMetadata = z.object({
})
export type AiMetadata = z.infer<typeof AiMetadata>

export const ActionClassification = z.enum(['READ', 'WRITE'])
export const ActionClassification = z.enum(['READ', 'SEARCH', 'WRITE', 'DESTRUCTIVE'])
export type ActionClassification = z.infer<typeof ActionClassification>

export const READ_ONLY_CLASSIFICATIONS: readonly ActionClassification[] = ['READ', 'SEARCH']

export const isReadOnlyClassification = (classification: ActionClassification | undefined): boolean =>
classification !== undefined && READ_ONLY_CLASSIFICATIONS.includes(classification)

export const PropertyGroupDisplay = z.enum(['tabs', 'section', 'summary', 'builder', 'footer'])
export type PropertyGroupDisplay = z.infer<typeof PropertyGroupDisplay>

Expand Down
Loading
Loading