Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
26 KiB
SCRATCHPAD — Workflow Data Store
Working notes, locked decisions, and implementation sequence. See PRD.md for the authoritative spec.
Locked decisions (with rationale)
- Two primitives, built together —
workflow_data_store(KV) +workflow_entity_links(mapping). The cross-run gap (V2 runs have only per-runvars) needs durable storage; KV covers "remember a value", links cover "map A↔B with reverse lookup". KV-only would force two hand-synced keys for reverse lookups. - Free-form namespaces — created on first write, no registry. A
*.list_namespaceshelper gives designer autocomplete without governance overhead. - N:M links; uniqueness on the typed edge —
UNIQUE (tenant, namespace, left_type, left_id, right_type, right_id, relation). The (left,right) pair is deliberately NOT unique, so two workflows can establish two connection types (e.g.mirrors+blocks) between the same entities; re-running a typed edge stays idempotent. Starting permissive here is safe; tightening later would be the risky direction, so this is the right default. - Single CE migration, distribute inline — workflow V2 tables already live in CE (
20251221090000). For a new table the house pattern is one CE migration that creates withtenant uuidand distributes behind anisCitusEnabledguard (transaction:false), templateserver/migrations/20260429133000_create_asset_facts_table.cjs. No CE/EE split — that split on the existing V2 tables was a retrofit (expand/contracttenant_id text→tenant uuid), not the greenfield pattern. Colocate withworkflow_runs→ group 41. created_by_run_idis a soft reference (no FK) — store/link data outlives runs (different retention); we don't want it cascade-deleted when a run is pruned.- Explicit-node writes only — dedicated
store.set/links.upsertnodes the author drops in deliberately. No per-actionpersistfield / auto-capture: the designer is already complex and a persist affordance on every action would add confusing surface everywhere. - Values are any string/number/boolean/JSON, incl. whole webhook/trigger payloads (
{ $expr: "payload" }), bounded by a 256 KB cap. Link ids are arbitrarytext. - RBAC reuses the existing
workflowresource — reads →workflow:read, writes →workflow:manage. No new resource, no permission-seed migration (rows already exist). Checked against the workflow actor. - No per-tenant row quota in v1 (revisit only if abuse appears).
- Designer UI =
@alga-psa/uicomponents only — creatable-combobox (TagInput/ComboBoxFieldSelector precedent) for namespace/type/relation,CustomSelectfor direction/value_type,ExpressionAutocompletefor ids, existing fixed-value/JSON editor forvalue. No native HTML controls. - Fully localized — strings via
t()undermsp/workflows; type/relation suggestion lists via theenum-labels-pattern.mdoption-hook (VALUES+LABEL_DEFAULTS+useXOptions()), storing the canonical token and translating the label; keys in all 9 locales +xx/yy;validate-translations.cjsmust pass.
Referencing model (same mechanism, two scopes)
- Same run: plain
vars(no DB) for pure data passing; the store is for durability.store.get/links.lookupwithsaveAsload intovars, referenced via{ $expr: "vars.x" }. - Cross run: a separate execution reads the same
(tenant, namespace, key)row identically. This is the only way to share state across runs.
Implementation sequence (suggested slices)
- Migration (
server/migrations/2026XXXX_create_workflow_data_store_tables.cjs) — both tables + inline distribution, mirroringasset_facts. → F001 - Models —
shared/workflow/persistence/workflowDataStoreModel.ts,workflowEntityLinkModel.ts. → F002, F003 - Actions —
runtime/actions/businessOperations/dataStore.ts,entityLinks.ts; register fromruntime/init.ts. → F004–F012, F015 - Limits + TTL — value/size caps; lazy expiry + janitor sweep on
WorkflowRuntimeV2Worker. → F013, F014 - Designer — catalog group + node config UI (design-system components). → F016, F017
- i18n — keys + option hooks across locales. → F018, F019
- Reference workflows + tests — fixtures + model/action/Citus/integration/i18n suites. → F020, F021, F022
Open items / to confirm during build
- Exact home for
valueJSON/payload editor component reuse (confirm the designer's existing fixed-value/JSON editor handles arbitrary objects, not just scalars). - Confirm how existing action
ui.label/descriptionare localized in the palette sostore.*/links.*follow the same wiring. - Decide the curated default lists (entity types, relations) — seed values in the option-hook constants.
- Migration timestamp to be assigned at implementation time (must sort after latest existing migration).
Notes
- Knowledge from analysis: V1 (event-sourced) runtime was removed (
20260308173000); everything here targets V2 (Temporal interpreter + DB-poll fallback + Redis event stream). The store is read/written purely through actions, so it is engine-agnostic.
2026-06-04 implementation log
F001 — CE migration
- Added
server/migrations/20260604120000_create_workflow_data_store_tables.cjs. - Migration creates both
workflow_data_storeandworkflow_entity_linkswithtenant uuidas the first column and FK totenants.tenant, composite PKs includingtenant, and all unique/index keys tenant-leading. - KV table includes unique logical identity
(tenant, namespace, key), namespace index, partial TTL sweep index(tenant, expires_at) WHERE expires_at IS NOT NULL,revision, optionalexpires_at, softcreated_by_run_id, and timestamps. - Link table includes N:M typed-edge uniqueness
(tenant, namespace, left_type, left_id, right_type, right_id, relation), forward/reverse lookup indexes, JSONBattributes, softcreated_by_run_id, and timestamps. - Distribution follows the asset facts pattern:
exports.config = { transaction: false }, Citus extension guard, already-distributed guard viapg_dist_partition, andcreate_distributed_table(..., 'tenant', colocate_with => 'workflow_runs'). Non-Citus CE/dev path logs a warning and leaves plain Postgres tables. - Down migration drops links before KV. No FK to workflow runs was added by design, because persisted store/link data must outlive run retention.
F002 — KV persistence model
- Added
shared/workflow/persistence/workflowDataStoreModel.tsand exported it from the persistence index. - Implemented tenant-explicit
get,set,delete,increment,list,listNamespaces, anddeleteExpired. setuses insert-ignore then update, returning{record, created, conflict}so callers can translate CAS mismatch into the action-layerCONFLICT.if_revision: 0is create-only; otherif_revisionvalues update only matching revisions.incrementis a single Postgres upsert and updates only existing numeric JSONB values, so concurrent increments are atomic and non-numeric values fail instead of being silently coerced.getalready performs lazy TTL behavior: expired rows are deleted opportunistically and returned as not-found. The worker sweep remains open under F014.listuses offset cursors to match existing workflow list patterns and filters expired rows; namespace counts also ignore expired rows.
F003 — entity-link persistence model
- Added
shared/workflow/persistence/workflowEntityLinkModel.tsand exported it from the persistence index. - Implemented tenant-explicit
upsert,lookup,delete,list, andlistNamespaces. upsertde-duplicates only the full typed edge(tenant, namespace, left_type, left_id, right_type, right_id, relation)and updatesattributeson replay, preserving the PRD's N:M behavior and allowing the same pair under different relations.lookupsupportsforward,reverse, andeither, returning target-side{link_id,type,id,relation,attributes}matches and applyingrelationplus target-type filtering.deleteenforces the safety rule that at least one side (leftorright) must be provided; namespace and optional relation always scope the deletion.
F004 — store.get action
- Added
store.getinshared/workflow/runtime/actions/businessOperations/dataStore.ts. - Read action is non-side-effectful, tenant-scoped through
withTenantTransaction, checksworkflow:read, and returns{found,value,value_type,revision,expires_at}. The model handles lazy TTL deletion, so expired rows returnfound:false.
F005 — store.set action
- Added
store.setwithvalue_type,ttl_seconds,if_revision, optionalidempotency_key, action-provided idempotency,workflow:manage, and audit operationstore.set. - CAS conflicts from the model are translated to
ActionErrorcodeCONFLICT.
F006 — store.delete action
- Added
store.deletewith action-provided idempotency,workflow:manage, audit operationstore.delete, and{deleted}output.
F007 — store.increment action
- Added
store.incrementwith atomic model increment,by/initial, action-provided idempotency,workflow:manage, audit operationstore.increment, and{value,revision}output. - Existing non-numeric values are rejected as validation errors instead of coerced.
F008 — store list actions
- Added
store.listwith namespace/prefix/limit/cursor pagination andstore.list_namespaceswith key counts. - Both are non-side-effectful reads and require
workflow:read.
F009 — links.upsert action
- Added
links.upsertinshared/workflow/runtime/actions/businessOperations/entityLinks.ts. - Write action is idempotent on the typed edge through the model, uses action-provided idempotency, requires
workflow:manage, auditslinks.upsert, and returns{link_id,created}.
F010 — links.lookup action
- Added
links.lookupwithdirection, optionalrelation/target-type filtering,limit,workflow:read, and{matches:[{link_id,type,id,relation,attributes}]}output.
F011 — links delete/list actions
- Added
links.deletewith left/right criteria validation, optional relation,workflow:manage, audit operationlinks.delete, and{deleted_count}. - Added
links.listandlinks.list_namespacesas non-side-effectful reads requiringworkflow:read.
F012 — shared action conventions
- All write actions use
withTenantTransaction,requirePermission(... workflow:manage),writeRunAudit,sideEffectful:true, andactionProvidedKey. - Read actions are
sideEffectful:false, usewithTenantTransaction, requireworkflow:read, and do not write audit rows.
F013 — action limits
- Added Zod length caps of 256 chars for namespace/key/entity ids/type/relation/idempotency inputs.
- Added
WORKFLOW_STORE_MAX_VALUE_BYTES(default 256 KiB) enforcement forstore.setbefore persistence, returningValidationErrorwhen exceeded.
F015 — runtime registration
- Registered
registerDataStoreActions()andregisterEntityLinkActions()fromshared/workflow/runtime/actions/registerBusinessOperationsActions.ts, soinitializeWorkflowRuntimeV2()loads them with the rest of business operations.
F014 — TTL worker sweep
- Added a throttled expired-row sweep to
shared/workflow/workers/WorkflowRuntimeV2Worker.ts. - Each tick calls
sweepExpiredWorkflowDataStore()only afterWORKFLOW_STORE_EXPIRY_SWEEP_INTERVAL_MS(default 60s). It selects tenants with expiredworkflow_data_storerows and callsWorkflowDataStoreModel.deleteExpired(knex, tenant, batchSize)per tenant. - Sweep bounds are configurable with
WORKFLOW_STORE_EXPIRY_SWEEP_TENANT_LIMIT(default 50 tenants per sweep) andWORKFLOW_STORE_EXPIRY_SWEEP_BATCH_SIZE(default 1000 rows per tenant). Errors are logged as warnings and do not fail workflow polling. - This completes TTL behavior together with the previously implemented lazy read expiry in
WorkflowDataStoreModel.get/store.get.
F016 — designer catalog and schema metadata
- Added a built-in
data-storedesigner catalog group inshared/workflow/runtime/designer/actionCatalog.tswith modulesstoreandlinks, default actionstore.get, and adata-storeicon token. - Added a Data Store palette icon mapping in
ee/server/src/components/workflow-designer/WorkflowDesigner.tsxusing the existing lucideDatabaseicon. - Extended workflow JSON-schema editor metadata with a typed
softEnumblock for creatable combobox fields. The metadata supports namespace suggestions viastore.list_namespaces/links.list_namespaces, curated entity type/relation token suggestions, custom free-text values, and namespace-scoped suggestion hints. - Updated
store.*andlinks.*schemas so namespace/type/relation/id/value fields expose designer editor hints: soft-enum custom inputs for labels, expression-capable text inputs for ids/keys, JSON editor hint forstore.set.value, and enum-backed fields (direction,value_type) remain fixed-select compatible through existing enum handling.
F017 — designer soft-enum UI
- Updated
ee/server/src/components/workflow-designer/mapping/InputMappingEditor.tsxso string fields withx-workflow-editor.softEnum.component === 'soft-enum-combobox'render the design-systemSearchableSelectwithallowCustomValue. - Curated token suggestions from metadata are shown for entity
typeand linkrelation; the current custom value is preserved as an option. Namespace fields accept free text and are ready for dynamic namespace suggestions once an autocomplete data source is wired. - Fixed choices (
direction,value_type) continue through the existing enum path, which rendersCustomSelect; ids/keys use the existing source-mode/expression editor plus@alga-psa/uiInputin fixed mode; JSON values use the existing JSON editor path. - No native HTML controls were added in the workflow designer code for Data Store fields.
F018 — Data Store designer localization
- Added Data Store palette group keys, all
store.*/links.*action label/description keys, soft-enum combobox placeholder/empty/custom-value keys, and entity type/relation enum label keys to everyserver/public/locales/*/msp/workflows.jsonfile. - Pseudo-locales
xxandyyuse marker text for the new Data Store keys instead of English fallback text, so the designer surface exposes missing i18n wiring during pseudo-locale QA. node scripts/validate-translations.cjspassed with zero errors; it still reports eight pre-existing extra-key warnings in Polishmsp/admin.jsonandmsp/settings.json, unrelated to workflow Data Store keys.
F019 — localized entity type/relation options
- Added
WORKFLOW_ENTITY_TYPE_VALUES/WORKFLOW_ENTITY_TYPE_LABEL_DEFAULTSandWORKFLOW_LINK_RELATION_VALUES/WORKFLOW_LINK_RELATION_LABEL_DEFAULTSinee/packages/workflows/src/constants/workflowEnums.ts. - Added
useWorkflowEntityTypeOptions()anduseWorkflowLinkRelationOptions()inee/packages/workflows/src/hooks/useWorkflowEnumOptions.ts, following the existing enum-label hook pattern. The stored values remain canonical tokens (for exampleproject_task,mirrors) while labels come frommsp/workflows. - Updated the Data Store soft-enum renderer to use those localized hooks for
workflow-entity-typeandworkflow-link-relationmetadata, preserving custom values as literal labels. - Removed the duplicated curated type/relation arrays from
shared/workflow/runtime/actions/businessOperations/entityLinks.ts; shared schema metadata now identifies the suggestion kind and the client owns localized display labels.
F020 — project-task mirror reference workflows
- Added
ee/test-data/workflow-bundles/workflow-data-store-task-mirror.v1.jsonwith two workflow-bundle entries:reference.project-task-mirror-link-setup:PROJECT_TASK_CREATED->projects.create_task->links.upsert, saving the created mirror task asvars.createdMirrorTaskand using that same-run output to persist the source/target edge.reference.project-task-mirror-sync:PROJECT_TASK_UPDATED->links.lookup->control.forEachovervars.linkedTasks.matches->projects.update_task.
- Added
payload.ProjectTaskUpdated.v1to the workflow payload schema registry viaprojectTaskUpdatedEventPayloadSchema;PROJECT_TASK_UPDATEDalready exists as a project task domain/webhook event, but workflow schemas previously exposed only created/status/assignment/completion task variants. - Verification: JSON fixture parses; focused ESLint and
tsc --noEmitchecks passed forshared/workflow/runtime/schemas/projectEventSchemas.tsandworkflowEventPayloadSchemas.ts.
F021/F022 — model/action/designer/i18n test coverage
- Added DB-backed persistence model tests in
shared/workflow/runtime/actions/__tests__/workflowDataStoreModels.db.test.tscovering KV CRUD/revision/CAS, atomic increment, TTL lazy expiry +deleteExpired, link idempotent upsert/N:M lookup/delete, and tenant isolation. - Added DB-backed business-operation action tests in
shared/workflow/runtime/actions/__tests__/businessOperations.workflowDataStore.db.test.tscoveringstore.*andlinks.*handlers, audit writes, oversize value rejection, permission denial, and action-provided idempotency key behavior. - Added action registration/schema metadata tests in
shared/workflow/runtime/actions/__tests__/registerDataStoreActionsMetadata.test.ts, designer catalog coverage inshared/workflow/runtime/__tests__/workflowDesignerActionCatalog.test.ts, and migration contract coverage inshared/workflow/runtime/__tests__/workflowDataStoreMigration.test.ts. - Added runnable designer/i18n/reference workflow contracts:
workflowDataStoreDesignerComponents.test.tsasserts the soft-enum branch uses design-systemSearchableSelectwith custom values and no native<select>.workflowDataStoreEnumLocalization.test.tschecks all workflow locales/pseudo-locales contain Data Store action/group/enum keys and that pseudo-locales do not fall back to English labels.workflowDataStoreReferenceWorkflows.test.tsvalidates the link-setup and mirror workflow bundle plus payload schema refs.
- Test-discovered implementation fix:
WorkflowDataStoreModel.setnow JSON-encodes values before writingjsonb, so string values persist as valid JSON strings;incrementnow casts boundinitial/byparameters before addition to avoid Postgres ambiguous-operator errors. - Commands run:
npx vitest run --config shared/vitest.config.ts workflow/runtime/actions/__tests__/registerDataStoreActionsMetadata.test.ts workflow/runtime/__tests__/workflowDataStoreMigration.test.ts workflow/runtime/__tests__/workflowDesignerActionCatalog.test.ts --reporter=dotDB_PASSWORD_ADMIN=devpassword123 DB_PASSWORD_SERVER=devpassword123 npx vitest run --config shared/vitest.config.ts workflow/runtime/actions/__tests__/workflowDataStoreModels.db.test.ts workflow/runtime/actions/__tests__/businessOperations.workflowDataStore.db.test.ts --reporter=dotnpx vitest run --config shared/vitest.config.ts workflow/runtime/__tests__/workflowDataStoreEnumLocalization.test.ts workflow/runtime/__tests__/workflowDataStoreReferenceWorkflows.test.ts --reporter=dotnpx vitest run --config shared/vitest.config.ts workflow/runtime/__tests__/workflowDataStoreDesignerComponents.test.ts --reporter=dotnpx vitest run --config shared/vitest.config.ts workflow/runtime/actions/__tests__/registerDataStoreActionsMetadata.test.ts workflow/runtime/__tests__/workflowDataStoreMigration.test.ts workflow/runtime/__tests__/workflowDesignerActionCatalog.test.ts workflow/runtime/__tests__/workflowDataStoreEnumLocalization.test.ts workflow/runtime/__tests__/workflowDataStoreReferenceWorkflows.test.ts workflow/runtime/__tests__/workflowDataStoreDesignerComponents.test.ts --reporter=dotnode scripts/validate-translations.cjs(passed; eight pre-existing Polish extra-key warnings remain outside workflow Data Store keys).- Focused ESLint on touched model/test files passed with warnings only (
no-explicit-any/non-null assertions in test files).
- Gotcha: the EE server jsdom harness currently resolves
next-auth->next/serverbefore a focusedInputMappingEditorStructuredLiterals.test.tsxinvocation can run, so the Data Store designer guard lives in the shared source-level component contract instead of adding a non-running render test to that EE file.
2026-06-05 audit remediation log
A full branch audit found three "finishing layer" gaps (core migration/models/actions verified correct). All three are now closed:
-
i18n — real translations (was F018 PARTIAL). The new Data Store keys had been inserted as English placeholders in the 7 shipping locales. Translated all keys (11 action label+desc pairs, palette group, 4 soft-enum strings, 7 entity types, 6 relations) in
fr/de/es/it/nl/pl/pt, matching each file's existing conventions (keep "workflow" in fr/de/it/nl/pt, translate in es/pl; tenant → locataire/Mandant/inquilino/dzierżawca, kept in it/nl/pt).xx/yypseudo-markers andenuntouched.validate-translations.cjsstill passes;workflowDataStoreEnumLocalization.test.tsgreen. -
Dynamic namespace suggestions (was a half-built designer feature). The soft-enum metadata declared
suggestionActionIds/namespaceFieldbut nothing consumed them. Added server actionlistWorkflowDataStoreNamespacesAction(ee/packages/workflows/src/actions/workflow-runtime-v2-actions.ts) —withAuth+requireWorkflowPermission(read), returns the deduped sorted union ofWorkflowDataStoreModel.listNamespaces+WorkflowEntityLinkModel.listNamespacesfor the session tenant. Wired intoInputMappingEditor.tsx: a module-level promise cache + auseEffect(fires only for theworkflow-data-store-namespacesoft-enum) loads the namespaces and merges them into the combobox options. Free-text + curated suggestions still work; the namespace combobox now also autocompletes from namespaces already used by the tenant. -
End-to-end engine integration test (was F022/T015 PARTIAL). Added
shared/workflow/runtime/actions/__tests__/workflowEngineReferenceWorkflows.db.test.ts: boots the realWorkflowRuntimeV2engine (NOT mockedshared), seeds tenant + actor + realworkflow:read/workflow:manageRBAC, registers two definitions, runs link-setup (forEach upserts N links) then a SEPARATE mirror run (links.lookup→control.forEachover matches →store.set). Asserts N links, cross-run handoff producing one store row per matched target, the lookup envelope match count, and real audit rows. Plus a no-match no-op case. Uses store/link primitives in place of the heavyweightprojects.*actions so the test stays hermetic while exercising identical engine mechanics (namespace/relation/entity-type identical to production). 2/2 pass against local Postgres in ~18s.
- Type-fix found during typecheck: the F017 soft-enum
<SearchableSelect options={options}>typedoptionsasCustomSelect'sSelectOption(labelstring | JSX.Element) which is not assignable toSearchableSelect'sSelectOption(labelstring). Re-typed the local array withSearchableSelect's option type.tsc -p ee/serverandtsc -p ee/packages/workflowsnow both report 0 errors. - Updated
workflowDataStoreDesignerComponents.test.tsimport assertion from an exact-string match to a regex (the SearchableSelect import line gained atype SelectOptionimport). - Verification:
tscclean (ee/server 0, ee/packages/workflows 0);validate-translations.cjsPASSED; full Data Store suite green (9 files / 42 tests incl. 3 DB suites) viaDB_PASSWORD_ADMIN=devpassword123 DB_PASSWORD_SERVER=devpassword123 npx vitest run --config shared/vitest.config.ts ....
2026-06-05 designer bug fix + normie UX pass
Surfaced while manually testing in the designer (screenshot showed links.upsert's right field as a bare "string").
-
$refrendering bug (real, not stale code).leftandrightshared oneentityRefSchemainstance, sozodToJsonSchemacollapsed the second into{$ref: …/left}. The designer's field editor does not resolve$ref, sorightrendered as an untyped "string". Fixed at the converter:zodToWorkflowJsonSchema(shared/workflow/runtime/jsonSchemaMetadata.ts) now passes$refStrategy: 'none'so every designer field schema is inlined — fixes any action with a reused field type, not just links. Regression test added inregisterDataStoreActionsMetadata.test.ts(serializedlinks.upserthas no$ref;from/toare objects withid/type). -
Normie-friendly field names + help text (scope: labels + help only). Renamed the author-facing link fields
left→from,right→to(and lookup'sright_type→to_type); the outputlinkItemOutputSchemanow also usesfrom/to. The DB columns and theWorkflowEntityLinkModelAPI stayleft_*/right_*— the handlers mapinput.from→modelleft,input.to→modelright. Rewrote allstore.*/links.*field descriptions in plain language (Collection / Record type / Record id / Relationship / etc.) and describedidempotency_key,if_revision,ttl_seconds,by,initialas advanced/optional with guidance. No new "advanced" input-bucketing mechanism was added (none exists; that "Advanced Options" in the UI is step-level retry settings) —idempotency_keyis flagged via its description instead.- Did NOT add a generic friendly-label (title) layer (the user declined that scope), so field labels show the schema key (
from,to,collection-via-namespace,relation) — clear, just lowercase. Field descriptions remain English-only, consistent with every other action in the designer (per-field descriptions are not i18n'd anywhere yet); a separate effort if we want them localized. - Updated the reference bundle (
workflow-data-store-task-mirror.v1.json), the e2e engine test, the bundle-shape test, the businessOps DB test, and the metadata tests tofrom/to.
- Did NOT add a generic friendly-label (title) layer (the user declined that scope), so field labels show the schema key (
- Verification:
tsc -p ee/packages/workflows0 errors; full Data Store suite green again (9 files / 43 tests). All changes are inshared/ee/packages/ee/server+ locales, so the running dev server must be restarted to pick them up.