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
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ Open-source AI-first workflow automation platform. Self-hosted or cloud. 400+ pi

## Query Error Handling

- **Global error dialog via `meta`** — `app.tsx` has a `QueryCache.onError` handler that shows an error dialog when `query.meta?.showErrorDialog` is truthy. When adding a new `useQuery` that fetches primary page data (e.g. table rows, list data), add `meta: { showErrorDialog: true }` to the query options.
- **Do NOT add** `showErrorDialog` to minor/auxiliary queries (feature flags, piece metadata, single-item fetches, filter options, user details). These should fail silently.
- Rule of thumb: if the query failure would leave the user staring at an empty table or blank page with no explanation, it should have `meta: { showErrorDialog: true }`.
- **A failed fetch is reported in place, never as a toast.** When adding a `useQuery` that fetches primary page data (table rows, list data), render `DataFetchErrorState` (`components/custom/data-fetch-error-state.tsx`) where the rows would go: pass `isError` / `errorStateEntity` / `onRetry` to `DataTable`, or branch on `isError` ahead of the empty state in a custom list. `errorStateEntity` is the already-translated, lowercase noun that reads inside "Trouble loading {entity}".
- **There is no global error toast.** `QueryCache.onError` in `query-client.ts` only `console.error`s. A toast on top of the placeholder is two notifications for one failure, and a toast on its own leaves an empty table behind that reads as data loss.
- **Do NOT add** an error state to minor/auxiliary queries (feature flags, piece metadata, single-item fetches, filter options, user details). These should fail silently.
- Rule of thumb: if the query failure would leave the user staring at an empty table or blank page with no explanation, that surface needs the placeholder.

## Key Utilities (`@activepieces/shared`)

Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ Open-source AI-first workflow automation platform. Self-hosted or cloud. 400+ pi

## Query Error Handling

- **Global error dialog via `meta`** — `app.tsx` has a `QueryCache.onError` handler that shows an error dialog when `query.meta?.showErrorDialog` is truthy. When adding a new `useQuery` that fetches primary page data (e.g. table rows, list data), add `meta: { showErrorDialog: true }` to the query options.
- **Do NOT add** `showErrorDialog` to minor/auxiliary queries (feature flags, piece metadata, single-item fetches, filter options, user details). These should fail silently.
- Rule of thumb: if the query failure would leave the user staring at an empty table or blank page with no explanation, it should have `meta: { showErrorDialog: true }`.
- **A failed fetch is reported in place, never as a toast.** When adding a `useQuery` that fetches primary page data (table rows, list data), render `DataFetchErrorState` (`components/custom/data-fetch-error-state.tsx`) where the rows would go: pass `isError` / `errorStateEntity` / `onRetry` to `DataTable`, or branch on `isError` ahead of the empty state in a custom list. `errorStateEntity` is the already-translated, lowercase noun that reads inside "Trouble loading {entity}".
- **There is no global error toast.** `QueryCache.onError` in `query-client.ts` only `console.error`s. A toast on top of the placeholder is two notifications for one failure, and a toast on its own leaves an empty table behind that reads as data loss.
- **Do NOT add** an error state to minor/auxiliary queries (feature flags, piece metadata, single-item fetches, filter options, user details). These should fail silently.
- Rule of thumb: if the query failure would leave the user staring at an empty table or blank page with no explanation, that surface needs the placeholder.

## Key Utilities (`@activepieces/shared`)

Expand Down
2 changes: 1 addition & 1 deletion brain/knowledge/ai-intelligence/ai-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ renders and preserves every real price; the cheapest in the set is 0.01.
- **Attribution headers are for `ACTIVEPIECES` only, and go through the factory's `extraHeaders` option rather than a local `createOpenRouter` call.** The managed provider is OpenRouter under the hood on our own key, so the `x-ap-*` headers are what tag *our* account's events: `x-ap-platform-id` / `x-ap-conversation-id` / `x-ap-run-id` on the agent path, `x-ap-project-id` / `x-ap-flow-id` / `x-ap-run-id` on the piece path. BYOK `OPENROUTER` is a customer's own account and must not get them. Constructing the provider inline to attach headers is also what silently drops `openRouterSettings` (the web-search plugin), since the factory is the only place that still passes them. (`CUSTOM` separately receives the piece-path metadata headers — that is #11700's metadata forwarding for self-hosted OpenAI-compatible endpoints, older than either the rename or the Autumn work and unrelated to OpenRouter attribution. Its precedence is deliberate: admin-configured `defaultHeaders` override the `x-ap-*` metadata, and the api key is applied last.)
- **`mistralViaOpenRouter` does not mean "the managed provider"; it is read only inside the `MISTRAL` case, and that branch looks like dead legacy.** `ACTIVEPIECES` routes through OpenRouter unconditionally and ignores the flag, so the only thing the agent path's `mistralViaOpenRouter: true` does is send a `MISTRAL` chat row to openrouter.ai — carrying that row's *Mistral* key, which cannot authenticate there. `MISTRAL` also has no `ALLOWED_CHAT_MODELS_BY_PROVIDER` entry, so `getCuratedChatModels` returns `undefined` for it and the resolver falls back to a tier's OpenRouter-shaped id. The fall-through arrived as a drive-by in #13489, not as a routing decision. Don't infer "this provider is AP-managed" from that case group.
- **AI Tool Configs** are a *sibling* feature (same `ai/` dir), distinct from AI Providers: they give the chat assistant external capabilities via `/v1/ai-tools` (platform-admin, EE/Cloud). **AiToolCapability** = `WEB_SEARCH`/`WEB_SCRAPING`/`IMAGE_GENERATION`; **AiToolProvider** = `TAVILY`/`FIRECRAWL`/`APIFY`/`FAL`. One config per capability (unique on platformId+capability); consumed by chat via `getEnabledTools()`. **Because the config is per-platform, it can never serve a first-run flow on Cloud.** A self-serve signup lands on a brand-new platform with no configs at all, so `getEnabledTools()` returns `{}` for exactly the users a new-signup feature is aimed at, and any capability read from it silently no-ops rather than failing loudly. A capability that has to work for someone who just signed up needs a cloud-wide `AppSystemProp` key instead, the way `TURNSTILE_SECRET_KEY`, `FEATUREBASE_API_KEY` and `APPSUMO_TOKEN` are sourced. Note there is no `ENRICHMENT` capability here, so anything needing people or company enrichment has nowhere to read a key from today.
- **`/v1/ai-tools` is registered only in the CLOUD and ENTERPRISE branches of `app.ts`, but the AI Center page that reads it is not edition-gated** — so a Community admin opening the Capabilities tab fired `useAiToolConfigs`, got Fastify's `Route not found`, and the query's `meta.showErrorDialog` popped the global "Failed to load data" dialog. Shipped that way from #13911 until the tab was gated on `ApFlagId.EDITION` in the page. Two things make this class of bug hard to place: the dialog is opened from `QueryCache.onError` in `query-client.ts`, so it is page-independent, and React Query's 3 default retries mean it lands several seconds later on whatever page you navigated to next (the report was against `/platform/setup/general`). When a screenshot's edition is in doubt, read the sidebar: **Billing & subscription** and **Usage** carry a lock only when `edition === COMMUNITY`, every other lock there is plan-driven. Any new EE-only route needs its UI entry point gated the same way, `enabled:` on the query or hiding the surface.
- **`/v1/ai-tools` is registered only in the CLOUD and ENTERPRISE branches of `app.ts`, but the AI Center page that reads it is not edition-gated** — so a Community admin opening the Capabilities tab fired `useAiToolConfigs`, got Fastify's `Route not found`, and the page showed the global "Failed to load data" dialog of the day. Shipped that way from #13911 until the tab was gated on `ApFlagId.EDITION` in the page. What made this class of bug hard to place is that the surface was then page-independent (raised from `QueryCache.onError` in `query-client.ts`) and React Query's 3 default retries meant it landed several seconds later on whatever page you navigated to next (the report was against `/platform/setup/general`). The tab now renders `DataFetchErrorState` in place instead, so a repeat would at least accuse the right page. When a screenshot's edition is in doubt, read the sidebar: **Billing & subscription** and **Usage** carry a lock only when `edition === COMMUNITY`, every other lock there is plan-driven. Any new EE-only route needs its UI entry point gated the same way, `enabled:` on the query or hiding the surface.

### Key files

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
status: accepted
---

# A failed fetch is reported in place, never as a toast

## Decision
When a query for a page's primary data fails, the surface that would have shown the rows shows `DataFetchErrorState` instead — a calm placeholder naming the entity, saying the data is safe, offering Try again. Nothing global fires: `QueryCache.onError` in `app/query-client.ts` only `console.error`s, and query `meta` carries no error flag at all.

## Context
This surface has now been rebuilt three times against the same customer report — a failed fetch reading as deleted data. First a blocking modal with the raw JSON payload (`meta.showErrorDialog`), which turned a 404 from an EE-only route into a wall of technical text. Then a global toast keyed on `meta.errorToastEntity`, which named what failed but still left the empty table sitting behind it. Both were page-independent, so React Query's three retries landed them seconds later on whatever page the user had moved to.

## Why
An empty table is the actual bug, and only the table can fix it. Once every list renders its own placeholder, a toast is either a second notification for one failure or — when it fires alone — an explanation floating next to an unexplained blank. Rejected: keeping the toast but firing it only when the query still holds cached data, so the two could never appear together. It is the more precise design and it covers a real gap (React Query keeps rendering stale rows after a failed refetch, and no placeholder can appear in that state), but it keeps a whole subsystem — meta typing, a `WeakSet` dedupe, an `isActive()` guard, an entity noun threaded through every query — alive to serve one case, and the team chose the smaller surface.

## Consequences
A silently-stale list is the accepted cost: if a refetch fails while data is cached, the rows stay and nothing says they are old. Losing the meta flag also removed the only marker a lint rule could have keyed on, so enforcement moved to the type system instead: `isError` and `errorStateEntity` are **required** props on `DataTable`, and the compiler refuses any table that has not decided. That is deliberately stronger than a lint rule — it fires while the component is being written, and it immediately surfaced four tables that had silently gone without an error state. A table whose rows are already-loaded props rather than its own query answers `isError={false}`, which is a statement rather than an omission. Hand-written lists (automations, agents, the AI Center tabs, the platform MCP page, the embed subdomain steps, the health runs tab) have no equivalent guard and rely on review until a `QueryBoundary` wrapper exists.

On one of those, forgetting the placeholder now fails silently and actively misleads: with no toast left, a failed query leaves `data` undefined, the component falls through to its *empty* state, and the app tells the user "No connections found" — an affirmative claim that their data is gone, which is the exact illusion this decision exists to prevent. The only remaining signal is the Sentry report from `QueryCache.onError`, and that is Cloud-only: `errorReporting` initialises from the `FRONTEND_SENTRY_DSN` flag, so on a self-hosted instance the report sits in a buffer that never flushes and the failure is invisible end to end.
2 changes: 1 addition & 1 deletion brain/knowledge/engineering/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The **Activepieces engineering brain**: how the system works, and *why* it was b
- **Engineering Handbook & Playbooks** — how we build and ship
- **API & Endpoints** — route conventions and the security contract
- **Server Module Anatomy** — the six files of a server module (entity → migration → repo → service → controller → module), and the manual registration steps nothing auto-discovers
- **Web Feature Anatomy** — the frontend feature folder, its barrel, route guards, and when a query gets the global error dialog
- **Web Feature Anatomy** — the frontend feature folder, its barrel, route guards, and how a failed primary query reports itself in place
- **Cloud Deployment Paths** — canary → prod, the `cloud-hotfix` override, and the breaking-migration gate that blocks both
- **Helm Chart** — the Kubernetes install we ship to self-hosters, its two competing paths for an `AP_*` variable, and the secrets it never creates
- **CI PR Review Hygiene** — draft-first Greptile review, the per-area PR size gate, and the workflow conventions reviewers keep re-litigating
Expand Down
Loading
Loading