We are building an ingest app on SuperRepo (beta): a user tags the entities a source document mentions (customers, devices, sites — types imported read-only from an Ontology Manager ontology of ~95 types) and one function-backed action commits creates, updates and links across about ten imported types plus three code-born ones (document, provenance row, receipt). Everything below was found on the **local preview**; several questions ask what holds when deployed. All but one gap have a workaround (G11 is an accepted degradation); none blocked us. The underscore link-id failure we posted earlier is fixed in `@osdk/maker` 0.67.0 — thank you. Every name below is a stand-in.
## Environment
- foundry-cli 0.253.0 (local ontology server `lohi-lib` 0.635.0, platform-api-proxy) · macOS 27.0 arm64 · Node 25.9.0 (`.nvmrc` pins 24) · pnpm 11.12.0
- `@osdk/maker` 0.67.0 · `@osdk/maker-experimental` 0.61.0 · `@osdk/maker-import` 0.37.0 · `@osdk/generator-converters.preview` 0.45.0 · `@osdk/generator-converters.ontologyir` 2.67.0 · `@osdk/generator` 2.67.0 · `@osdk/seed-helpers` 0.27.0
- `@osdk/client` / `@osdk/api` 2.57.0 (workspace override; maker 0.67.0 declares `@osdk/api ~2.67.0` but builds against it) · `@osdk/functions` 1.20.0 · `@foundry/functions-typescript-runtime` 0.299.0 · `@foundry/functions-typescript-osdk-discovery` 1.35.0
- `@osdk/vite-plugin-superrepo` 0.10.0 (dev smart client)
- Imports via `foundry import ontology`; new types code-born under `com.example.app.*`.
## Summary
| # | Component (version) | Symptom | Local workaround |
|—|—|—|—|
| G1 | `@osdk/maker-experimental` 0.61.0 `api/defineOntologyV2.js:37` | Link with an imported end: imported side gets a temp RID → `generate-sdk: Unable to find entity` | `void importedType.apiName` at module top level |
| G2 | preview converter + local ontology server 0.635.0 | Once built, a link with an imported end is not served (M2M and 1:N); for M2M, no reverse side in the OSDK and writes get `400` | Code-born join object + property filters |
| G3 | maker-experimental `cli/generateBackingDataset.js:311` | `includeEmptyBackingDatasource` on such a link: `Could not find object types for link` | Omit the flag (leaves an unbound dataset input) |
| G4 | maker-experimental `cli/main.js:126` | Block-data function discovery sees no imported types → `unsupportedType` → function-backed action cannot register | Code-born-only declared return type + cast |
| G5 | functions runtime 0.299.0 `PropertyTypeConverter` ↔ local `metadata.json` | Array and geopoint edits fail on every type | Omit locally behind a positive preview check; report the omission |
| G9 | functions runtime ↔ local ontology server | Timestamp edits rejected (`Ontologies:InvalidPropertyValue`) | Same guard, by property name |
| G6 | local ontology server | Non-atomic apply; duplicate PK → `500 Default:Internal` | Validation-first commit; receipt is the last edit |
| G10 | local ontology server | A failed persist can answer `200` with zero edits | App confirms success by reading the receipt back |
| G7 | `@osdk/vite-plugin-superrepo` 0.10.0 `smartClient.transformResponseFromLocal` | Query `UserFacingError` text lost; struct results still wire-wrapped | Errors as data; JSON-string returns |
| G8 | local ontology server | Same-type M2M links stored side-swapped; intermediary links are plain tables | Avoid directional self-links; write relationship rows |
| G11 | functions runtime / `@osdk/functions` 1.20.0 | No calling-user identity reaches a function locally (live route not tested) | None (no user attribution on the function’s writes) |
| G12 | platform-api-proxy + local ontology server | Media sets cannot be written locally (`501` / `404` / `400`); attachments work | Attachment property |
## 1. Links with an imported end (G1–G3)
```ts
import { customer } from “#generated-imports”;
defineLink({ apiName: “documentCustomer”,
many: { object: IngestDocument, metadata: { apiName: “customers” } },
toMany: { object: customer, metadata: { apiName: “documents” } },
editsEnabled: true, includeEmptyBackingDatasource: true });
```
### G1 — imported link ends get temporary RIDs
**Observed:** `generate-sdk … ERROR Unable to find entity`; the IR names the imported side `ri.ontology-metadata.temp.object-type.`. `OntologyRidGeneratorImpl` snapshots `getImportedTypes()` at construction (`defineOntologyV2.js:37`); import stubs are lazy proxies registered on first property access, which `convertLink` triggers too late.
**Expected:** the stub resolves to its `ridHint`.
**Workaround:** `void customer.apiName;` at module top level.
**Question:** is eager stub registration or a lazy RID lookup an acceptable fix, and is the top-level `void` safe in deployed block data (it seems only to change registration order)?
### G2 — the built link is not served locally
**Repro:** G1 workaround applied; start the preview.
**Observed:**
```
GET objectTypes/com.example.app.ingestDocument/outgoingLinkTypes → 200 {“data”:[]}
GET objectTypes/customer/outgoingLinkTypes/documents → 404 Ontologies:LinkTypeNotFound
POST actions/…/apply (batch.link(doc, “customers”, customer))
→ 400 {“errorName”:“Default:InvalidArgument”,“parameters”:{“linkTypeRid”:“”,“objectType”:“com.example.app.ingestDocument”}}
```
A 1:N with an imported `one` side builds but is likewise not served (`404 LinkTypeNotFound` on the imported side; write not attempted). Imported↔imported and code-born↔code-born links work.
**Expected:** both sides served, `batch.link` accepted.
**Workaround:** a code-born join object `documentLink { id, documentId, targetType, targetPk }`, linked 1:N to `ingestDocument`, queried by `where({ targetType, targetPk })` plus a pivot.
**Question:** are links with an imported end meant to work locally? When deployed?
### G3 — `includeEmptyBackingDatasource` on that link fails the build
**Observed:** `Error: Could not find object types for link “documentCustomer”` — `generateBackingDataset.js:311` searches only code-born `ontologyIr.ontology.objectTypes`.
**Expected:** the flag binds the link’s datasource as for code-born object types.
**Workaround:** omit it; the datasource becomes an unbound block input (`inputs[“dataset-datasource-link.documentCustomer”]`) an install must map.
**Question:** should the flag be legal here, and what does the unbound input do at install?
## 2. Function discovery ignores imports (G4)
**Repro:** `commitIngest` declares its honest edit union (`Edits.Object | Edits.Link<customer, “device_owner_2”> | Edits.Object`) and a `defineFunctionBackedAction` names it.
**Observed:** `{“code”:“unsupportedType”,“message”:“\“Edits.Object\” is not a supported type.”,“severity”:“error”}`, then `ERROR Rebuild failed: Invariant failed: Function “commitIngest” not found in functions IR`; the whole ontology build fails and the previous one keeps serving. `cli/main.js:126` calls `getPreviewFullMetadataFromBlockData(blockDataJson)` without the import JSON; the SDK-stage pass, which gets imports, accepts the same function.
**Expected:** both passes see the same entities.
**Workaround:** declare code-born edits only and cast:
```ts
type Declared = Edits.Object; // batch is still createEditBatch(client)
return batch.getEdits() as unknown as Declared[];
```
The spec then lists `editedObjects: {“com.example.app.ingestReceipt”: {}}` only, yet the local server applies the `customer` edits — provenance is not enforced locally.
**Question:** does deployed Actions reject edits outside the declared provenance? If so this cast is a deploy-time trap and the import JSON at `main.js:126` is the real fix.
## 3. Property types the local runtime cannot write (G5, G9)
### G5 — arrays and geopoints, on any type
**Repro:** `batch.create(customer, { …, aliases: [“a”, “b”] })`; `batch.update(site, { location: { type: “Point”, coordinates: [10.0, 20.0] } })`.
**Observed:**
```
aliases → 400 Ontologies:ApplyActionFailed {“kind”:“runtimeError”,“message”:“Cannot read properties of undefined (reading ‘itemType’)”} PropertyTypeConverter.js:176
location → 400 Ontologies:ApplyActionFailed {“kind”:“runtimeError”,“message”:“Unexpected property type for ontology edits”} PropertyTypeConverter.js:250
```
The runtime’s `PropertyTypeConverter` expects OMS shapes (`array.itemType`, `geohash`); the build’s `.dev-server/var/conf/metadata.json` carries API-v2 shapes (`array.subType`, `geopoint`), consumed unconverted. Empty arrays pass (the per-item map callback never runs), so the fault is item-type conversion. Same under preview converter 0.31.0, so not a regression. Arrays and geopoints seed and read back fine, and arrays filter with `$contains`; only function writes fail.
**Expected:** function edits accept what the seed accepts.
**Workaround:** none without patching; our wrapper omits array/geopoint values **only** after positively detecting the preview (`GET /api/v2/ontologies` lists exactly one ontology, `ri.ontology.main.ontology.0`) and reports the omission. Any doubt ⇒ write everything.
**Questions:** does the deployed runtime get OMS-shaped metadata, making this local-only? Is a shim planned? Is there a supported way to detect the preview from inside a function?
### G9 — timestamps
**Repro:** a function edit setting a code-born `timestamp` property from an ISO-8601 string, e.g. `batch.create(ingestReceipt, { …, committedAt: new Date().toISOString() })`. For comparison, the seed validator (`@osdk/seed-helpers` 0.27.0) requires `timestamp` values as RFC 3339.
**Observed:** the runtime emits epoch millis; the local server accepts only ISO-8601 and rejects the edit as `Ontologies:InvalidPropertyValue` (value shown as `U64(…)`).
**Expected:** one agreed wire shape.
**Workaround:** the same guard, by property name.
**Question:** which shape does the deployed runtime/OMS pair use for timestamp edits — epoch millis or ISO-8601 — and will the local server accept what the runtime emits? Is there a supported way to write a timestamp from a local function today?
## 4. Apply semantics on the local server (G6, G10)
### G6 — non-atomic apply; duplicate PK is a 500
**Repro:** one batch creates the `ingestReceipt`, then a `customer` with an existing PK.
**Observed:**
```
POST actions/com.example.app.commit-ingest-action/apply
→ 500 {“errorName”:“Default:Internal”,“parameters”:{“objectType”:“customer”,“primaryKey”:“00000000-0000-4000-8000-000000000001”}}
server log: cause “Cannot add object that already exists in the store”
GET objects/com.example.app.ingestReceipt/ → 200 ← the first edit persisted
```
Edits apply in array order and stop mid-batch without rollback. The template’s notes say a plain create action returns `Ontologies:ObjectAlreadyExists` here; we did not re-verify that, but a function-backed action does not.
**Expected:** all-or-nothing, as documented for deployed actions, and a typed duplicate-PK error.
**Workaround:** all reads and validation (idempotency, natural-key collisions via `$in`) before the first edit; the receipt is the **last** edit, so a partial apply leaves none and a retry re-plans.
**Question:** are deployed function-backed actions editing imported types transactional? Could the local server roll back and return `ObjectAlreadyExists`?
### G10 — HTTP 200 with zero edits after a failed persist
- Observed during development: an apply whose edits did not persist answered `200` with an empty edit list, so locally a 200 is not proof of a write. We have not isolated a minimal repro to share.
**Expected:** a non-2xx status when persistence fails.
**Workaround:** the app treats a commit as successful only when the receipt reads back.
**Question:** is this known? A pointer to a server path that swallows a persist failure, or a condition yielding a 200 with no edits, would help us isolate a repro.
## 5. Error plumbing in the dev smart client (G7)
**Repro:** `throw new UserFacingError(“…”)` in a query called via `useOsdkFunction`.
**Observed:** the runtime returns `{“executionResult”:{“type”:“failed”,“failed”:{“type”:“userFacingError”,“userFacingError”:{“message”:“…”}}}}`, but `transformResponseFromLocal` reads only `failed.runtimeError.message`, so the app gets `Error(“Function execution failed with no error message”)`. REST, plain `@osdk/client` and actions over the Vite `/api` path all deliver `error.parameters.message` (`409 Ontologies:QueryEncounteredUserFacingError`, `400 Ontologies:ApplyActionFailed`). (The transform also unwraps only the top level, so struct returns should stay wire-wrapped — plugin source, not browser-confirmed.)
**Expected:** parity with the `/api` path.
**Workaround:** the preview query returns `{ok:false, errors:[…]}` as data; every function returns a JSON string.
**Question:** is a fix planned? Is `error.parameters.message` the stable location when deployed too (`error.message` is only `Failed to fetch 400 Bad Request`)?
## 6. Link storage quirks (G8)
**Repro:** `batch.link(a, “related_customer_2”, b)` on an imported same-type M2M.
**Observed:** reads back as `b.related_customer_2 → a`, `a.related_customer_1 → b`, `a.related_customer_2 → []`; cross-type links are unaffected. Separately, a live link backed by a relationship object (`DeviceSiteRelationship`) is served locally as an independent table: creating the row does not populate `device.sites`, while a direct `batch.link(device, “sites”, site)` succeeds.
**Expected:** direction preserved; intermediary links derived from their rows.
**Workaround:** avoid directional self-links until deployed; write relationship rows and read two-hop.
**Question:** is the side-swap local-only? Is a direct link on an intermediary link type rejected when deployed?
## 7. No current-user identity in functions, locally (G11)
**Observed:** `@osdk/functions` 1.20.0 exposes no identity API — only the `UserId`/`GroupId`/`Principal` *types*; `FunctionContext` is `{ foundryToken, traceId, spanId, parentSpanId, sampled }` and the local bearer (`mock` / `mock-token-for-local-dev` / `fake-local-dev-token`) carries no identity. The one route, Admin `GET /api/v2/admin/users/getCurrent`, sits in the template’s `platformApiProxy.passthrough`, so forwards to the live enrollment; we did not call it.
**Expected:** a fixed mock principal locally.
**Workaround:** no user attribution on the function’s writes; the platform’s edit history is the record of who committed.
**Question:** is there, or will there be, a supported way to read the calling user inside a function, locally and deployed? Should the template’s passthrough be documented as live traffic?
## 8. Media sets (G12)
**Repro:** `{ type: “mediaReference”, includeEmptyBackingMediaSet: true, editOnly: true }` on a code-born type; a create action with `{ fileName, data: Blob }`.
**Observed:** the client sends `PUT /api/v2/mediasets/media/upload?preview=true`; the proxy answers `501 PlatformApiProxyPathNotAllowlisted` (`x-foundry-platform-api-proxy: blocked`) with a `config_hint` to add the path to `passthrough` — which would ship the bytes to the live enrollment. Direct to the local server: `404` (no `/v2/mediasets/*`); `POST …/objectTypes//media//upload` → `400 Default:InvalidArgument`, `uploadMediaContent in Ontologies namespace not supported. Use uploadMedia in MediaSets namespace instead.` A fabricated `ri.mio.main.*` reference is accepted (`200`); `fetchMetadata` then gives `404 MediaSets:MediaItemNotFound`. Also: `mediaReference`/`attachment` on an `includeEmptyBackingDatasource` type need `editOnly: true`, and a `MediaReference` function parameter is silently dropped (use `Media` from `@osdk/api`). **Attachments work end to end** (plain, modify and function-backed actions; byte-exact to 30 MiB from Node and a browser `File`; readable in a function), though bytes do not survive a dev-loop restart and seeds cannot carry them.
**Expected:** a local media store, or a loud error.
**Workaround:** an attachment property behind a thin upload seam; media sets deferred to post-deploy.
**Questions:** is a local MediaSets store planned? Could the `config_hint` warn that the path is live traffic? Should the dropped parameter be an error?
—
## What would help adopters most
1. Local runtime parity for array, geopoint and timestamp edits (G5/G9), or a documented shim.
2. Import JSON passed to block-data function discovery (G4).
3. Links with an imported end served locally, with a deploy story for their datasource (G1–G3).
4. Smart client surfacing `userFacingError` text and unwrapping structs (G7).
5. Atomic local apply, `ObjectAlreadyExists` on duplicates, never a 200 on failure (G6/G10).
6. A mock identity for functions (G11) and a local media store (G12).
7. A documented “local preview limitations” page — half of this list would have been a lookup; we would gladly review a draft.
Happy to share minimal repros (synthetic data) for all but G10, for which we have not isolated one.