Reference brief for N build lanes. Grounds a Palantir-benchmarked, ontology-first, Cedar-PBAC, deterministic (no-AI), enterprise-production console. Every claim is live-sourced (URLs inline). Dense by design — read the area you're building, steal the pattern, follow the URL to verify.
Compiled 2026-07-09. Areas 1-3 (Foundry, Cedar, config-console) are the deepest.
Foundry (§1), Cedar (§2), config-consoles (§3), and half the modules (§4: Workday, Temporal, SAP GL, OAIS) all converge on the same primitive: an append-only, effective-dated / fixity-stamped event log, with current state DERIVED by folding immutable records — never mutated in place.
- Writes go through declarative, named mutation verbs (Foundry Actions, SAP balanced documents, Temporal Commands), landing in a writeback/event store, not direct table UPDATEs.
- Authorization is a policy over the object/row/field evaluated identically for UI, automation, and API (Foundry object/property policies; Cedar policy set) — deny-by-default.
- Config (schema, screens, rules, views) is itself data — a governed, versioned, immutable-per-deploy object.
Design everything below as a consumer of that substrate.
Principle to internalize: "single ontology, many consumers." One ontology is the shared semantic + kinetic layer; every consumer reads and writes through it, never around it. Object Explorer, Workshop apps, Automate rules, and API clients are all just consumers of one model — a human clicking an Action and an automation firing the same Action hit identical logic, validation, writeback, and security. Contrast the anti-pattern: N apps each with their own schema copy, their own writeback path, their own bolt-on permissions.
Sources: overview, core-concepts, create-object-type, action-types/overview, rules.
- Object type = schema for an entity/event; object = instance; object set = collection. Docs' own analogy: object type ≈ dataset, object ≈ row, properties = columns, property values = cells.
- The model splits into semantic (objects, properties, links) + kinetic (actions, functions, dynamic security). This split is load-bearing: the ontology is a data model plus the verbs that mutate it.
- Backing datasource: an object type is a typed projection over an existing dataset, not a new store. Model = pick backing dataset → declare primary key (unique per row, required) → declare title key (human display name) → map columns to typed properties.
- Link types: schema of a relationship; supports 1-1 / 1-many / many-many (many-many needs a join dataset). Traversable in UI (search-around) and code (ObjectSet search-around).
- Actions (writeback / kinetic mutation) — the ONLY sanctioned way to mutate:
- Action type = schema for a set of edits made atomically, + side effects.
- Edits land in the object type's writeback dataset (most-current data incl. user edits), propagated to every consumer immediately. This is how a read-only projection becomes editable.
- Parameters (user inputs; defaults, object-set-filtered dropdowns, security-aware dropdowns).
- Rules = automated behavior on submit; submission criteria = validation gating submit.
- Side effects: notifications, webhooks, build/schedule triggers, attachment uploads.
- Function-backed actions: run custom function code, with batched execution over many objects.
- Functions = TypeScript/Python logic on objects (computed values, action backing).
- Interfaces = polymorphism (object types sharing a shape addressed abstractly). Shared properties = a property definition reused across object types (centralized metadata).
Object security model (spans 1a–1c) — object-and-property-policies, restricted-views:
- Two axes: discretionary (granular policies you author) + mandatory controls (markings / orgs / classifications inherited from the datasource — cannot be discretionarily loosened).
- Object security policy = row-level (fail → whole instance hidden). Property security policy =
column-level (fail → that field returns
null, object still visible). Together = cell-level. - Configured in Ontology Manager Security tab, independent of backing-dataset permissions (a user needs only Viewer on the object-type definition). Constraints: PK can't be in a property policy; each non-PK property in ≤1 property policy.
- Maps to us: this is exactly the Cedar (§2) surface — object policy = row authz, property policy = field authz. Build both over the same policy set; deny by omission.
Sources: ontologies-proposals, review-ontology-proposals, foundry-branching/core-concepts.
- Object/link/action types authored in a UI (display name, types, PK, title key, security).
- Branching: changes on a branch off main; rebase to stay current.
- Proposals = pull requests for the ontology: create proposal → merge checks (conflict detection) → reviewers (approver must be editor/owner of the resource) → merge.
- Protection: once enabled, edits must go through branch + proposal (no direct-to-main).
- Changelog tab = full per-user, per-timestamp history.
- Maps to us: schema changes are code-reviewed and versioned like source. Governance = branch + proposal + merge-check + reviewer-approval + changelog. This IS the "config as governed object" pattern (§3E) applied to the ontology itself.
Sources: overview, search-syntax.
- Single search bar over the whole ontology; graph view shows link types between object-type groups.
- Search-around = link traversal as a first-class UI verb (hover a result → explore across a link).
- Linked-object filtering: filter the main object set on properties of linked types; charts aggregate a property on main or linked types. Actions runnable from the object view. Shareable deep-links.
Sources: concepts-widgets, concepts-variables, concepts-events.
- Widgets = UI building blocks placed in a layout, configured with input/output variables + Actions.
- Variables = typed data flow (object set, string, numeric, boolean, date, timestamp, array, struct,
geopoint, time-series). Init types: static, function, object property, object-set aggregation,
object-set definition (filter/traverse), variable transformation.
- Lazy computation: a variable computes only when a visible widget displays it. Recompute modes: automatic / event-triggered / load+event. Variable lineage graph shows widget↔variable deps.
- Events = trigger behavior on user action (button, row-select, dropdown, tabs).
- Two-way binding: widget params bind to variables (Workshop→widget); widget events bind to Workshop events and can update those params (widget→Workshop).
- Custom widgets (iframe): read/write Workshop variables + fire events via a documented bridge.
- Maps to us: an app is a declarative document — widgets + typed variables + event bindings + ontology Actions. No imperative app code. This is the target "per-app config as data" (see §3 for how the low-code world stores/versions this).
Sources: automate/overview, condition-objects, effect-actions.
- Model = Condition(s) → Effect(s), checked continuously or on schedule.
- Condition types: (1) time-based ("every Mon 9AM"); (2) object-data on the ontology ("new Alert with priority=high"); (3) combined. Object-set variants: objects added to set, objects modified in set, run on all objects (periodic sweep).
- Effects (4): Action execution (submit a Foundry Action on matching objects), Function invocation, Notifications (platform + email + attachments), Webhook/external API. Run sequentially or in parallel.
- Effect inputs: the exact triggering objects flow automatically as inputs to the effect.
- Latency modes: live / scheduled / automation-dependent (chain automations).
- Maps to us: monitors are defined over object sets (ontology queries), not raw tables, and the same Action types humans use are the effect verbs — human + automated paths share one mutation surface.
Sources: data-integration/overview, data-lineage/overview, building-pipelines/overview.
- Flow: Data Connection (syncs sources, credentials, governance; versioned via dataset transactions) → Pipeline Builder (visual pipelines, DQ, loading) → cleaned datasets → mapped to object/link types. Pipeline Builder can write directly to the ontology (output IS the object type's backing dataset).
- Data Lineage = interactive graph spanning source → dataset → object + link types → apps & automations, across project boundaries. Trace any object property back to the exact upstream transform.
Actionable re-implementation cues: (1) object types = typed views over existing tables with PK + title key, not a new store; (2) all writes go through declarative Action types → a writeback table, never direct UPDATEs; (3) security is a policy on the object/property, evaluated identically for UI, automation, API; (4) apps and automations are declarative documents (widgets+vars+events / conditions+effects) referencing ontology objects and Actions; (5) schema changes are branch+proposal+ merge-check+changelog.
Sources: terminology,
syntax-policy,
Context7 /cedar-policy/cedar-docs (templates, schema, patterns, best-practices),
partial-evaluation guide (Cedarland),
Cedar OOPSLA paper,
AWS prescriptive guidance.
A policy = effect + scope + optional conditions + optional annotations, ends with ;.
- Effect:
permitorforbid. - Scope (mandatory):
(principal, action, resource)— each can be unconstrained,== Entity, orin Group.actioncan bein [Action::"a", Action::"b"]. - Conditions:
when { ... }/unless { ... }over attributes of principal/resource/context.
permit ( principal, action == Action::"editPhoto", resource )
when { resource.owner == principal };
forbid ( principal, action, resource )
when { resource.private }
unless { principal == resource.owner };
- Allow iff at least one
permitmatches AND noforbidmatches. Otherwise Deny. - Deny by default / deny-by-omission: no matching permit → Deny.
forbidalways overridespermit— a single matching forbid denies regardless of any permits. This is exactly the enterprise "deny-by-default, explicit forbid as guardrail" posture. Model tenant-isolation, legal holds, and hard blocks asforbidpolicies (they can never be accidentally out-permitted).
Sources: human-readable-schema, Context7 patterns/best-practices.
- Schema declares entity types (with typed attributes +
tags), action types, and applies-to (which principal/resource types each action accepts). The validator uses the schema to catch policy errors before evaluation — treat schema as a compile-time contract; validate all policies in CI.
entity User in [Group] { personalGroup: Group, delegate?: User, blocked: Set<User> } tags String;
entity Group enum ["G1","G2","G3"];
- Entity hierarchy = the
in/memberOfTypesparent chain. A group is an entity with children;principal in Group::"x"tests membership transitively. Works for principals (users→groups), resources (files→folders), AND actions (viewPhoto ∈ readOnly).
This is the key modeling move for our PBAC direction (roles = principal attributes only):
- Model a role as a parent entity in the hierarchy, then scope the permit to it:
permit ( principal in Role::"ContractManager",
action in [Action::"reviewContract", Action::"executeContract"],
resource );
- Membership (who is a ContractManager) is managed as entity data, independent of the policy. Adding
a user to a role is a data write, not a policy edit. The engine indexes policies by the group and
skips them for principals not in that group (fast slicing). Best-practice doc: put the role/group in
the scope, not in a
whenclause, so it's indexable. - ABAC = add
when { principal.department == resource.department }. RBAC + ABAC compose in one policy set.
Sources: templates, Context7 json-format.
- Template = policy with named slots
?principal/?resource(only these two; slots allowed only in scope, not conditions). Like a SQL prepared statement. - A template-linked policy binds concrete entities to the slots at runtime. Editing the template updates all linked policies. Use for ad-hoc sharing / discretionary grants ("share ticket X with user Y") — link a template instead of writing N near-identical policies.
permit ( principal == ?principal, action in Action::"Shared_TicketAccess", resource == ?resource )
when { resource.status == "OPEN" };
Sources: Cedarland partial-eval guide, cedar_policy Rust docs.
is_authorized_partial(vsis_authorized) can return Allow, Deny, or a residual: the set of policies it couldn't fully evaluate because some input was leftunknown().- Residuals = constraints describing the principal's viewable set. Leave
resourceunknown → the residual is "the filter for every resource this principal may access." - You can translate a residual into a SQL WHERE clause and push it to the DB — the engine has already resolved everything the DB can't answer (e.g. "was the request MFA-authenticated"). This is how you do row-level filtering / list endpoints without evaluating per-row in a loop.
- Maps to our granularities:
- Screen/nav:
is_authorized(user, Action::"viewScreen::Foo", App::"console")→ deny-by-omission hides the nav entry. - Row: partial-eval with unknown resource → residual → SQL filter on the list query.
- Field: one action per sensitive field (
Action::"viewField::salary") or property-policy style; fail → null the field (mirror Foundry §1a). - Aggregation: authorize the aggregation action AND ensure the underlying row filter (residual) is applied before aggregating, so counts/sums never leak forbidden rows.
- Screen/nav:
Cedar best practices to enforce in CI: validate every policy against the schema (catches typos/type
errors pre-deploy); put roles/groups in scope (indexable) not conditions; use forbid for guardrails
(tenant isolation, holds) since forbid always wins; keep entity IDs stable/unique; deny-by-omission is
the default — never write a catch-all permit.
Every low-code platform independently arrived at the same shape. Steal the shape, not any one product.
Sources: Retool components + transformers, Appsmith writing-code, ToolJet control-components, Budibase data-provider, Windmill app_editor.
- Universal pattern: every component/query/variable is a globally-named object in a flat
namespace; UI props bind via a
{{ }}expression evaluated as JS. The set of{{ }}references forms a dependency graph that auto-reruns on input change (Retool, Appsmith, ToolJet, Budibase). - Keep the graph acyclic: reads are pure reactive bindings (Retool transformers can only
return, never mutate); writes are quarantined to explicit imperative actions (setValue/setIn, ToolJet CSAs). Adopt this read/write split. - Two data-flow shapes: standalone query objects components pull from by reference (Retool/Appsmith/ ToolJet — composable) vs a Data-Provider pushing context down the tree (Budibase — simple but couples fetch to position). For a governed console, prefer decoupled query-objects + named exposed variables.
- Fix a small set of binding namespaces — Windmill's Context / State / Component-outputs / Background is the cleanest articulation. Define these explicitly, not a free-for-all global.
Sources: Retool import-export, Appsmith JSON DSL, Windmill app_editor.
- Every platform stores the app as a serializable config document built client-side (Retool JSON/Toolscript ZIP, Appsmith JSON DSL, Windmill declarative tree, Budibase CouchDB docs).
- Split config per screen/page to survive concurrent editing — Retool deliberately stores component
positions in per-page
.positions.jsonto prevent merge conflicts. One document per screen, not one monolith. (Directly relevant given our unprotected-main + parallel-lane reality.)
Sources: Airtable field model, Notion property object. Airtable and Notion converged on a near-identical model — this is THE field-schema pattern:
- Every field =
{ id, name, type, config }wheretypeis a string tag andconfigis type-specific. ~35 types. New field types ship without migrations. - Options/choices are IDed sub-entities
{id, name, color}, referenced by id not label → rename-safe. - Computed fields reference other fields by id and nest a result-type (Airtable
result, Notion rollup) → computed types compose recursively. - Forward-compat is a hard requirement: Airtable explicitly states clients must gracefully handle
unknown
type. Build the reader to skip/degrade, never crash. - Schema-value mirroring: a record/page's stored values mirror the schema shape (value type derived from property type). One source of truth.
Sources: Airtable view config, Notion working-with-views.
- A view = serializable bundle of
{visible fields + order, filters, sorts, grouping, layout extras}bound to one table, fully decoupled from data. Hiding a field is view-scoped, never destructive. - Reuse one filter/sort AST at both view-level and ad-hoc query-level (Notion: a view IS a saved query). Define the filter/sort schema once.
- Per-view ownership/lock: Collaborative / Personal (per-user) / Locked. Plan this into the view object from day one.
Sources: ToolJet version-control + multi-environment, Windmill draft_and_deploy + roles/permissions, Retool source-control, ServiceNow Data Policy. Two dominant patterns for turning config into a governed, releasable artifact:
- Immutable-version + promote (ToolJet, Windmill) — recommended for us. Draft (editable) → Saved/Deployed = immutable checkpoint (Windmill content-addresses by hash); only immutable versions promote; sequential Dev→Staging→Prod; version artifact is env-agnostic, connection config is env-bound. Cleanest for audit + rollback (restore any past immutable version).
- Git-PR + separate release gate (Retool, Appsmith) — config history in git via PRs, but Releases are deliberately separate from git (git = history, Release Management = deploy gate).
- Drafts are per-user and invisible to the workspace (Windmill, ToolJet) — multiple editors draft the same item conflict-free; only deploy makes it authoritative. Replicate to avoid clobbering.
- Environment separation (unanimous): keep the app/version definition env-agnostic; a single resource/datasource object carries per-environment credentials. Never fork the app per env.
- Content-address deploys: immutable hash per deploy + a mutable path pointer to latest (Windmill). Free rollback + tamper-evidence. Ties directly to our audit-chain work.
- Decouple author from runtime identity:
permissioned_as≠created_by(Windmill) — config runs as a defined principal, not whoever last edited it. Directly relevant to Cedar/PBAC: the config object carries its own effective-principal, evaluated at execution → no privilege escalation via shared config. - Publish ≠ data migration: Budibase couples app-publish with internal-data replication (can overwrite prod data). Keep config-promotion and data-migration as separate pipelines.
Sources: ServiceNow UI Policy, race UI-policy vs client-script.
- UI Policy = a record, not code:
sys_ui_policy(conditions as data + table + Order + on-load / reverse-if-false flags). Field rules live in child records (sys_ui_policy_action), one per field, each tri-state Mandatory / Visible / Read-Only ∈ {True, False, Leave-alone}. - Tri-state "leave alone" is what lets multiple rule-sets compose non-destructively. Integer
Ordersequences policies (ascending, last-writer-wins on shared fields). Reverse-if-false auto-reverts actions when the condition goes false (declarative two-way binding). - Server-side twin (
sys_data_policy2, Data Policy): enforce the expressible subset (mandatory / read-only) on every write path (form/import/API), sharing the same condition-record shape. Never trust client-only config enforcement — mirror it server-side over the same data model.
- Inline schema editing (Airtable/Notion): adding a field is a data insert, not a code change.
- Autogenerated screens from schema (Budibase): read a table schema → generate CRUD UI → new table yields a working screen with zero build.
- Because layout, fields, views, and rules are all data documents, "add a screen/field/view/rule" is a governed write to the config store, flowing through the same draft→release pipeline — never a redeploy.
Immutable hash per deploy + mutable "latest" pointer · per-user invisible drafts · draft→review→
release gate · env-agnostic artifact + env-bound credentials · content-addressed rollback · config
carries its own effective-principal (Cedar) · hierarchical RBAC with a small fixed verb set
(Own/Edit/Use/None) attached to the config object's path/scope · server-side enforcement of every
client-declared rule · interpolation as an injection boundary (Windmill's locked $var:/$res:
grammar — no arbitrary eval at the secret boundary).
Each: (a) core model that makes it best-in-class, (b) one reusable takeaway.
-
Workday — Business Process framework + effective-dating. (a) Every HR action (hire, absence, time, staffing) is an event instance run through a configurable BP definition: ordered steps (approval/to-do/checklist/integration/notification), each gated by condition rules + routing modifiers, ending in a mandatory completion/commit step; all records effective-dated (time-sliced versions keyed by effective date). (b) Separate transaction data from the process that commits it: BP =
definition → steps[] → (condition, action, routing)+ one commit step; store every entity as append-only effective-dated rows (validity interval), not in-place updates → audit + retro edits + future-dating for free. datasheet -
Recruiting — Greenhouse (structured hiring). (a) Separates Candidate (person) from Application (candidacy for one Job/Req); each Application flows an ordered Stage pipeline; each stage has an Interview Kit; each interviewer files a Scorecard rating predefined attributes on a fixed rubric + overall rec. Mandatory structured scorecards make the hiring record defensible. (b) Schema:
Candidate 1─* Application *─1 Job;Job 1─* Stage(ordered) 1─1 InterviewKit;Application *─* Interviewer via Scorecard. Source-tracking lives on the Application, not the Candidate. scorecard overview -
Automation execution — Temporal (durable event history). (a) A workflow's whole lifecycle is a durable append-only Event History; worker code emits Commands (intent) → service maps each to an Event (durable outcome). Recovery = deterministic replay feeding recorded events back; already-recorded Activities are not re-executed (result replayed) → effectively-once side effects. (b) Log events, not snapshots; event =
{eventId monotonic, eventType, attributes}; rebuild state by folding the log; make code deterministic (replay/history mismatch = bug); expose timeline/full views over one log. event history -
Messaging — Slack (threads + Events API unfurl). (a) Message keyed by immutable
tswithin a channel; a thread = any message withthread_ts= parent'sts(flat, one-level). Unfurl is event-driven:link_shareddelivers metadata only (ts, channel, matched links, thread_ts) — never body; app replies viachat.unfurlwith a per-URL block map (links:writescope). (b) Model message PK(channel_id, ts)+ nullablethread_tsself-FK; rich previews = register-domains → metadata-only event → resolve → post-back (never trust event to carry content); cap registered domains to bound fan-out. link_shared · chat.unfurl -
Gmail — conversation threading + labels. (a) A message threads only when all three hold:
In-Reply-To(direct parent) +References(full ancestry) headers link prior messages,Subjectmatches (ignoringRe:-type prefixes), and samethreadId. Identical subject+sender alone will NOT thread without an explicitReferences/In-Reply-To. Labels ≠ folders: labels are many-to-many tags on a message. (b) Thread by header-chain first, normalized-subject + participants only as tiebreak; storeMessage *─* Labeljunction, derive a thread's labels as the union of members'. Gmail threads -
SAP — 3-way match, GL, PM work orders. (a) MM 3-way match: PO ↔ GR ↔ Invoice reconciled via a GR/IR clearing account — GR posts
dr inventory / cr GR-IR, invoice postsdr GR-IR / cr AP, netting to zero only when qty+price+product agree within tolerance (else invoice blocked). FI GL: every posting is a balanced document of line items where Σdebit = Σcredit. PM work order: status machineCRTD → REL (can book cost) → TECO (no further cost) → CLSD, forward-only. (b) Use a clearing/suspense account as the reconciliation join (3-way match = "does the clearing line net to zero within tolerance"); model every financial mutation as balanced header + line-items (Σdr=Σcr invariant); drive work orders through an explicit status profile with cost-posting gates. 3-way match -
Compliance — Vanta / Drata (control → evidence → continuous test). (a)
Control → Test → Evidence: each control backed by automated tests pulling evidence from connected systems on a schedule (hourly/daily); a cross-framework control mapping lets one evidence item satisfy overlapping requirements (SOC2 ∩ ISO 27001). (b) ModelControl *─* Test,Test → (integration, query, schedule) → Evidence(timestamped, retained),Requirement *─* Control; continuous = re-run on cron + diff. Vanta evidence · Drata monitoring -
Evidence/records standards — ISO 15489 / OAIS (ISO 14721) / FRE 902(13-14) / WORM. (a) ISO 15489: records must be authentic, reliable, integral (unaltered), usable, with metadata + retention/disposition schedules + audit trails. OAIS: six functions (Ingest, Archival Storage, Data Mgmt, Administration, Preservation Planning, Access); package as SIP→AIP→DIP where the AIP bundles content + PDI (provenance, context, reference, fixity). FRE 902(13)/(14): electronic records are self-authenticating via certification by a qualified person — 902(14) endorses proving a copy identical via matching hash values. WORM: write-once immutable retention storage under all of it. (b) On ingest: compute + persist a SHA-256 fixity hash per object, wrap with provenance/PDI into an immutable AIP-style package, write to WORM/object-lock for the retention window, keep a certification record — one design satisfies ISO 15489 integrity + OAIS fixity + FRE 902(14) hash self-authentication + WORM. (Directly informs our L20 audit-chain.) FRE 902 · ISO 14721
- One ontology, projected over Postgres tables (object type = typed view + PK + title key), not a second store. Every consumer (screens, automations, API) reads/writes through it. (§1)
- All mutations are declarative named Actions → writeback/event rows, never direct UPDATEs; state is folded from an append-only, effective-dated log (Workday/Temporal/SAP/OAIS all agree). (§0, §1a, §4)
- Cedar is the single authz surface for screen / row / field / aggregation: deny-by-default,
forbidfor tenant-isolation guardrails (always wins), roles = principal attributes in scope (indexable), partial-eval residuals → SQL WHERE for row-level list filtering. Validate every policy against schema in CI. (§2) — aligns with existing Cedar-activation + tenant-isolation gate work. - Screens/apps/automations/views/field-rules are governed config DATA — one JSON doc per screen, discriminated-union field schema (forward-compat on unknown types), views = saved filter/sort ASTs, field rules = ServiceNow-style tri-state records enforced BOTH client and server. (§3)
- Config is an immutable-versioned governed object: per-user drafts → review → released checkpoint (content-addressed hash) → env-agnostic artifact + env-bound creds → content-addressed rollback → config carries its own effective-principal for Cedar. Schema changes go branch+proposal+merge-check+ changelog like Foundry Ontology Manager. (§1b, §3e/h)
- Audit/evidence = fixity-hashed, WORM, certifiable (ISO 15489 / OAIS / FRE 902(14)) — the L20 chain.
Doc gaps flagged during research (verify against a live instance if you depend on them): Appsmith's
exact git repo layout + DB JSON schema; Retool's internal stored app schema (only export formats
documented); Windmill git_sync page 404'd (mechanics via CLI sync); some ServiceNow tri-state precedence
details lean on community articles; several ToolJet .ai/tooljet-concepts URLs 404 (canonical paths under
/docs/development-lifecycle/ + /docs/release-management/).