Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when migrating any Operate page from operate/client/ to the orchestration cluster webapp. Always read frontend-migrator first — this skill adds Operate-specific overrides, the migration loop protocol, and page-by-page context.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 237% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 140% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 233% | 0% |
> Read frontend-migrator first. This skill only documents what is different or specific to Operate.
Keep styled-components. The frontend-migrator skill says SCSS modules — ignore that for Operate. Port styled-components as-is and defer the ShadCN migration until after all pages are unified. The ShadCN migration will happen cross-pod in one coordinated sweep.
TypeScript:
const, map/filter/reduce over mutable patterns. Local let/for is fine for tight data aggregation where it reads clearer (see useRunningInstancesCount.ts).ProcessesPage.tsx exports ProcessesPage). Exception: a colocated query module may export both its queryOptions and its use* hook (see the shared HTTP layer reference below).Components:
Tests:
Page list, status, and per-PR breakdown live in GitHub: epic #51305 → one page subissue each → inner subissues per PR. Source path, target route, and fidelity scope live in each page issue body. Query live (see "State lives in GitHub" below); never cache here.
Operate route files live under webapp/client/apps/orchestration-cluster-webapp/src/routes/_auth/operate/. The route guard (route.tsx) and empty shell (index.tsx) already exist — do not recreate them.
Operate has ~20 stores. Most are transient UI state and do not need porting. Map each one:
| Store | What it holds | Target | |-------|--------------|--------| | authentication.ts | Session | Already in #/shared/auth/ — reuse | | currentTheme.ts | Theme preference | Already in #/shared/theme/ — reuse | | variableFilter.ts | Filter inputs on Processes page | URL search params via validateSearch on the route | | instancesSelection.ts | Selected rows | useState inside the page component | | panelStates.ts | Which panel is open/collapsed | useState | | dateRangePopover.ts | Calendar open/close | useState | | executionCountToggle.ts | Toggle state | useState | | incidentsPanelFiltersStore.ts | Filter inputs on Incidents tab | URL search params | | modifications.ts | Pending variable modifications (complex) | useState + local reducer — or keep as MobX if truly complex | | batchModification.ts | Batch operation in-progress | useState | | processInstanceMigration.ts | Migration wizard state | useState + URL params for step | | diagramOverlays.ts | Diagram overlay data | useState inside BPMN component | | networkReconnectionHandler.ts | Connectivity polling | Port to a standalone hook with useEffect | | notifications.tsx | Toast queue | notificationsStore from #/shared/notifications/notifications.store — already exists, reuse |
Decision rule: Ask "Would the user want to share/bookmark this state?" → URL search params. "Is it ephemeral per-visit?" → useState. "Is it server data?" → TanStack Query.
Endpoints go in #/shared/http/endpoints.ts. Queries go in #/shared/http/queries.ts.
queries.ts is a thin registry — queryKey + queryFn for a single HTTP request, nothing else. Never add refetchInterval, staleTime, gcTime, aggregation logic, or multi-page fetch logic here. These belong in the component or a component-local hook.
| Concern | Where it goes | |---------|--------------| | Polling (refetchInterval) | useSuspenseQuery({...query(), refetchInterval: N}) at the call site, or in a local hook | | Multi-page fetching | Local hook — export a queryOptions function for route prefetching + a useSomething() hook for the component | | Aggregation / data transformation | select option on useSuspenseQuery, or inside the local hook's queryFn |
Reference implementation: operate/pages/Dashboard/useRunningInstancesCount.ts — exports both runningInstancesCountQuery() (used in the route loader for prefetching — data goes in loader, never beforeLoad, which is reserved for guards/redirects; see routes/_auth/operate/index.tsx and docs/monorepo-docs/frontend/data-loading.md) and useRunningInstancesCount() (used in the component). The route imports the query function; the component imports the hook. queries.ts stays thin.
Pattern (copy from existing entries in those files):
ts// endpoints.ts import {endpoints as api} from '@camunda/camunda-api-zod-schemas/8.10'; const endpoints = { // existing entries... searchProcessInstances: (body: SearchProcessInstancesRequest) => new Request(getFullURL(api.searchProcessInstances.getUrl()), { ...BASE_REQUEST_OPTIONS, method: api.searchProcessInstances.method, body: JSON.stringify(body), headers: {'Content-Type': 'application/json'}, }), };
ts// queries.ts const queries = { // existing entries... searchProcessInstances: (params: SearchProcessInstancesRequest) => queryOptions({ queryKey: ['searchProcessInstances', params] as const, queryFn: async () => { const {response, error} = await request(endpoints.searchProcessInstances(params)); if (error !== null) throw error; return response.json() as Promise<SearchProcessInstancesResponse>; }, }), };
Check @camunda/camunda-api-zod-schemas/8.10 first before writing a custom endpoint — many Operate endpoints are already there. Import endpoints from the package to get the URL and method.
The codebase does not use TanStack Query's useMutation. Follow the Tasklist patterns by write complexity:
request(endpoints.xxx(...)) in the event handler, then queryClient.invalidateQueries({queryKey: [...]}) for affected lists.setup + fromPromise actors) that receives queryClient as input. Reference: tasklist/modules/task-details/taskCompletionMachine.ts —queryClient.setQueryData(...), with rollback on failurequeryClient.fetchQuery(queries.xxx()) until it leaves the transitional statequeryClient.invalidateQueries(...) for affected list queries on completionrefetchInterval (see POLLING_STATES in routes/_auth/tasklist/_tasks/$userTaskKey/route.tsx)Operate's batch operations (cancel/retry/delete, batch modification) follow the accepted → pending → completed lifecycle, so expect the machine pattern there. Do not put write logic in queries.ts — it stays a read-only registry.
Operate strings go under operate.* inside the shared translation namespace:
json// shared/i18n/locales/en.json — inside "translation": { … } "operate": { "dashboard": { "title": "Dashboard" }, "processes": { "title": "Processes" }, "decisions": { "title": "Decisions" }, "operationsLog": { "title": "Operations Log" }, "batchOperations": { "title": "Batch Operations" } }
Usage: const {t} = useTranslation(); t('operate.dashboard.title')
Add all 4 locales (en/de/fr/es) — LLM-translate de/fr/es and note "LLM-translated — native speaker review requested" in the PR description.
ts// SomePage.test.tsx import {it} from '#/vitest-modules/test-extend'; import {renderWithRouter} from '#/vitest-modules/render-with-router'; import {mockSomeEndpoint} from '#/shared-test-modules/mock-handlers'; import {createSomeEntity} from '#/shared-test-modules/api-mocks/some-entities'; import {userEvent} from 'vitest/browser'; import {HttpResponse} from 'msw'; // worker is injected and auto-managed by the `it` fixture — no beforeAll/afterAll needed it('should display process instances', async ({worker}) => { worker.use( mockSomeEndpoint({successResponse: HttpResponse.json({items: [createSomeEntity()], totalCount: 1})}), ); const screen = await renderWithRouter(SomePage, {path: '/operate'}); await userEvent.click(screen.getByRole('button', {name: 'Expand'})); await expect.element(screen.getByText('Dashboard')).toBeVisible(); });
Add new endpoint mocks to shared-test-modules/mock-handlers.ts using createEndpointMock(). Never inline http.post(...) directly in test files.
Response fixtures come from factories in shared-test-modules/api-mocks/ (createBatchOperation, createUserTask, …). Add a factory there when mocking a new entity — never build response literals inline in tests.
POST/PUT/PATCH mocks must validate the request payload: pass {schema, successResponse, failureResponse} — the mock returns failureResponse when the request body fails the Zod schema, so tests catch malformed payloads instead of green-lighting them. See mockCompleteTaskEndpoint usage in tasklist/pages/TaskDetailsTaskPage.test.tsx (it extends the API schema to pin exact expected variables).
Interactions use userEvent from 'vitest/browser' (userEvent.click, userEvent.fill, userEvent.keyboard); direct locator.click() is acceptable for simple clicks.
Run from webapp/client. Gates 1–6 are local; 7–9 gate the PR (9 is CI-authoritative — verify locally, never push regenerated snapshots). Use the existing package scripts — gates 1, 2, 6 also run together via npm run lint.
npm run lint:prettiernpm run lint:eslintnpm run typecheck -w @camunda/orchestration-cluster-webappnpm run test:unit -w @camunda/orchestration-cluster-webappnpm run build -w @camunda/orchestration-cluster-webappnpm run lint:knipnpm run test:integration -w @camunda/orchestration-cluster-webappnpm run test:a11y -w @camunda/orchestration-cluster-webappIterate against feedback signals in three tiers, by cost. Loop on the cheapest tier that can fail; graduate only when green.
| Tier | Gates | Loop on it when | Max iterations | |------|-------|-----------------|----------------| | edit | 1 Prettier · 2 ESLint · 3 Typecheck | after every meaningful edit (seconds) | 5 | | component | + 4 Unit · 5 Build · 6 Knip | a component is done (minutes) | 5 | | PR | 7 Integration · 8 a11y · 9 Visual (CI) | before marking ready — drive with ci-fix-failure | 3 |
Stop condition (guardrail). Each tier loop is bounded. If a tier is not green within its max iterations, stop and report — do not keep iterating. A loop with no bound spins forever and burns budget on a problem it cannot converge on; the cap forces escalation to the engineer instead. An iteration that makes zero progress (same failure, same fix attempted) counts double — bail early.
Full loop, start to close:
read this skill + the page issue (gh, live) load spec + state
→ own the inner subissue for this PR (set in progress)
→ port the component (legacy = exact spec, 1:1)
→ [edit tier] loop until green
→ [fidelity] deterministic checks + LLM flagger (below)
→ [component tier] loop until green
→ open DRAFT PR title feat:/fix:/refactor: …; body "Closes #<inner-subissue>"; request Copilot review
→ push → [PR tier] ci-fix-failure loop until CI green
→ close the loop check off the inner subissue, update the page issueEpic #51305 → page subissue (sibling naming Migrate Operate <Page> page to unified webapp) → inner subissue per PR (conventional-commit naming, one PR each). Never cache issue/PR state in a file; query it:
gh issue view <n> --repo camunda/camunda --json title,body,stategh api repos/camunda/camunda/issues/<n>/sub_issuesgh pr list --repo camunda/camunda --search "<page>"Open the PR as a draft, body Closes #<inner-subissue>, then request Copilot review. Keep it draft until all 9 gates are green and Copilot threads resolved; only then mark ready.
gh pr create --draft --title "<conventional-commit>" --body "Closes #<n>"copilot-review memory).Every new failure mode becomes a rule, not a one-off fix: encode it in this skill or your Claude memory so it cannot recur.
Run after the edit tier, scoped to the just-ported component — not across the whole operate/ dir, or you flag not-yet-ported features.
Deterministic (script, always trusted): run from the repo root (the script and its default locales path are repo-root relative — not webapp/client):
bashnode .claude/skills/frontend-operate-migrator/scripts/fidelity.mjs \ --ported <ported-component-dir>
Checks locale coverage (every t('operate.*') key exists in en/de/fr/es). Non-zero exit = a gate failure; fix before continuing.
Judgment (LLM flagger — flag, never approve): the two checks a script cannot make. Emit a reviewable diff for the engineer; never assert "looks faithful."
legacy → port diff of observable behavior. The engineer decides; you do not.Per the verification rule: a script saying "key X missing from de.json" is trusted; an LLM saying "looks faithful" is not. The flagger produces evidence, the engineer rules.
feat: migrate Operate <PageName> page to unified apppackage.json for any package you import; add if missing, never rely on transitive deps.beforeLoad = auth/guards only; loader = data prefetch (see docs/monorepo-docs/frontend/data-loading.md).<feature>.queries.ts exporting queryOptions; shared/http/queries.ts is cross-app only.shared-test-modules/api-mocks/ first; new mocks go in shared-test-modules/mock-handlers.ts only.tsconfig.browser.json types before touching global.d.ts; vite/client covers *.svg.@camunda/camunda-api-zod-schemas/8.10 before writing a custom endpoint.useSuspenseInfiniteQuery; trust hasMoreTotalItems, prefer cursor over offset.x-eventually-consistent in the spec → add refetchInterval (1s fresh, 5s batch, slower otherwise); pessimistic UI.GET /v2/batch-operations/{key}. Submit toast, poll in background, never block the page.| Kind | Use for | |------|---------| | Route params ($key) | Entity identity (/processes/$processKey) | | Search params (validateSearch + Zod) | View state: filters, sort, cursor, selection, active tab, modal-open flag | | Local React state | Ephemeral UI only: open menu, input draft, hover, focus |
Validate every search/path param with Zod via validateSearch / parseParams. Reuse @camunda/camunda-api-zod-schemas shapes when they map to an API contract.
The orchestration cluster webapp does not use Mixpanel tracking. Do not port legacy tracking events, tracking-only state, or tracking tests. If a callback combines tracking with feature behavior, preserve the feature behavior and remove only the tracking code.
Gate unfinished features in src/shared/feature-flags.ts (SCREAMING_SNAKE_CASE, default false). Gate at the highest level (route, page, nav item), not deep inside modules. Remove the flag in a dedicated cleanup PR once the feature ships.
Other measured skills in the registry, with their headline benchmark lift.