Widget Types and When to Use Each
@osdk/widget.client package and run directly in the Workshop page sandbox, giving them native access to Workshop variables and events with up to 50 parameters and 50 events. They support full Object Set sizes and are the recommended path for all new widget development.
Bidirectional iFrame Widgets use @osdk/workshop-iframe-custom-widget to wrap an existing React application inside an <iframe> element. A postMessage-based bridge connects the iFrame to Workshop via the useWorkshopContext hook. The trade-off is a hard 10,000 object limit on Object Set parameters and no support for struct-typed variables. Use this path only when you need to integrate a pre-existing React app without a full rewrite.
OSDK React Apps are standalone applications deployed through the Developer Console at their own URL. They use @osdk/client directly and have no Workshop variable or event constraints. They are appropriate for tools that do not need to live inside a Workshop dashboard — for example, administrative portals or data entry forms that navigate independently.| Type | Framework | Workshop Integration | Object Limit | Key Package |
|---|---|---|---|---|
| Custom Widget | React (any version) | Full — 50 params, 50 events | No hard limit | @osdk/widget.client |
| Bidirectional iFrame Widget | React (any version) | Bridge — params and events via postMessage | 10,000 objects | @osdk/workshop-iframe-custom-widget |
| OSDK React App | React (any version) | None — standalone URL | No hard limit | @osdk/client |
import { defineWidget, Parameters } from '@osdk/widget.client';
const parameters = {
title: Parameters.string(),
maxItems: Parameters.number(),
showBorder: Parameters.boolean(),
selectedObjects: Parameters.objectSet({ objectType: 'Employee' }),
} satisfies Parameters.Definition;
export default defineWidget({
parameters,
render: ({ parameters }) => {
return {
element: document.getElementById('root')!,
component: MyWidgetApp,
props: {
title: parameters.title.value ?? 'Default Title',
maxItems: parameters.maxItems.value ?? 25,
showBorder: parameters.showBorder.value ?? false,
selectedObjects: parameters.selectedObjects.value,
},
};
},
});⚠️ 10,000 object limit
Bidirectional iFrame Widgets silently truncate Object Set parameters at 10,000 objects. There is no error thrown — your component simply receives fewer objects than exist. If your widget needs to work with large Object Sets, use a Custom Widget instead. The Custom Widget sandbox has no such cap and streams objects through the Workshop variable binding layer.
Parameters and Events
Parameters namespace from @osdk/widget.client and include primitive types (boolean, number, string, date, timestamp), Object Set bindings, and array variants of all primitive types. Workshop binds values to parameters through the widget configuration panel; the bound values are available at runtime via parameters.<id>.value.
Events are the outputs a Custom Widget sends back to Workshop. They are declared similarly using the Events namespace and can carry typed payloads. In the React binding layer (@osdk/widget.client-react), events are emitted by calling the emit function returned from useEmitEvent. Workshop can subscribe to these events to update variables or trigger Actions.
A widget is limited to 50 parameters and 50 events. These counts include all parameter types combined. For most widgets this is generous, but complex dashboards that duplicate many filter parameters can approach the limit. If you hit the ceiling, consolidate related parameters into a JSON string parameter and parse it in the widget.| Type | TypeScript Type | Example Value |
|---|---|---|
| boolean | boolean | undefined | true |
| number | number | undefined | 42 |
| string | string | undefined | "hello" |
| date | string | undefined (ISO date) | "2024-03-15" |
| timestamp | string | undefined (ISO 8601) | "2024-03-15T10:30:00Z" |
| objectSet | ObjectSet<T> | undefined | ObjectSet<Employee> |
| array (string) | string[] | undefined | ["a", "b"] |
| array (number) | number[] | undefined | [1, 2, 3] |
| array (boolean) | boolean[] | undefined | [true, false] |
import React from 'react';
import { useParameterValue, useEmitEvent } from '@osdk/widget.client-react';
import { defineWidget, Parameters, Events } from '@osdk/widget.client';
const parameters = {
employeeSet: Parameters.objectSet({ objectType: 'Employee' }),
filterActive: Parameters.boolean(),
} satisfies Parameters.Definition;
const events = {
employeeSelected: Events.objectClicked({ objectType: 'Employee' }),
filterChanged: Events.string(),
} satisfies Events.Definition;
export default defineWidget({ parameters, events });
export function EmployeeListWidget() {
const employeeSet = useParameterValue('employeeSet');
const filterActive = useParameterValue('filterActive');
const emitEmployeeSelected = useEmitEvent('employeeSelected');
const emitFilterChanged = useEmitEvent('filterChanged');
const handleEmployeeClick = (employee: Employee) => {
emitEmployeeSelected({ object: employee });
};
const handleFilterToggle = (value: string) => {
emitFilterChanged(value);
};
return (
<div>
{/* render employee list */}
</div>
);
}⚠️ Parameter ID mismatch
Parameter IDs must be camelCase and must match exactly between the widget definition object and any reference in code. A typo in the ID causes Workshop to silently ignore the binding — no error is thrown, the parameter value is simply always `undefined`. Always define parameter IDs as string literals in one place and reference them via the object key, not a separate string constant that can drift.
State Management in Custom Widgets
localStorage, sessionStorage, and IndexedDB. Any call to these APIs will throw a security error at runtime. This means you cannot persist state the way a typical web app would — all state that needs to survive navigation, tab switches, or page refreshes must be stored externally.
The correct persistence target depends on the state type. User selections and filter values that should survive across sessions belong in Workshop string or boolean variables bound as parameters. The widget writes back to these variables by emitting typed events that Workshop subscribers update. For heavier computed results, consider caching in an Ontology object via a Function-backed Action rather than re-computing on every render.
React's useState and useRef are perfectly fine for ephemeral UI state (open/closed dropdowns, animation phase, in-progress form values before submission). Understand clearly which state is ephemeral and which must persist — the boundary is whether the user would be surprised to lose it on a tab switch.| State Type | Store In | Example |
|---|---|---|
| User selections | Workshop string/boolean variable | Selected tab ID, active filter name |
| Filters / search terms | Workshop string variable (emit event on change) | Search input value, date range string |
| Current view / navigation | Workshop string variable | Active panel ID ("detail" | "list") |
| Form drafts | Workshop string variable (JSON-serialized) | Partially filled form as JSON string |
| Heavy computation results | Ontology object via Action | Pre-aggregated report stored as object property |
import React, { useCallback } from 'react';
import { useParameterValue, useEmitEvent } from '@osdk/widget.client-react';
import { defineWidget, Parameters, Events } from '@osdk/widget.client';
const parameters = {
activeTab: Parameters.string(),
searchQuery: Parameters.string(),
} satisfies Parameters.Definition;
const events = {
tabChanged: Events.string(),
searchQueryChanged: Events.string(),
} satisfies Events.Definition;
export default defineWidget({ parameters, events });
export function TabbedWidget() {
const activeTab = useParameterValue('activeTab') ?? 'overview';
const searchQuery = useParameterValue('searchQuery') ?? '';
const emitTabChanged = useEmitEvent('tabChanged');
const emitSearchQueryChanged = useEmitEvent('searchQueryChanged');
const handleTabChange = useCallback(
(tabId: string) => {
emitTabChanged(tabId);
},
[emitTabChanged],
);
const handleSearchChange = useCallback(
(query: string) => {
emitSearchQueryChanged(query);
},
[emitSearchQueryChanged],
);
return (
<div>
<TabBar active={activeTab} onChange={handleTabChange} />
<SearchBar value={searchQuery} onChange={handleSearchChange} />
</div>
);
}⚠️ State lost on tab switch
When a user navigates away from a Workshop tab and returns, the widget iframe is torn down and re-mounted. All React state (useState, useRef, context) is wiped. Any value that was not persisted to a Workshop variable will reset to its initial value. Users experience this as the widget "forgetting" what they were doing. Design for this from the start — treat in-memory React state as purely ephemeral and route anything meaningful through Workshop variable events.
Bidirectional iFrame Widget Integration
@osdk/workshop-iframe-custom-widget package injects a useWorkshopContext hook into your React app that provides access to Workshop parameters and event emitters via a postMessage bridge between the iFrame and the parent Workshop page.
Installation requires adding @osdk/workshop-iframe-custom-widget to your app's dependencies and wrapping your top-level component (or a subtree) with the WorkshopContextProvider. The useWorkshopContext hook then returns the current parameter values and emit functions.
Critical limitations to understand before choosing this path: Object Set parameters are capped at 10,000 objects with no workaround. Struct-typed Workshop variables cannot be passed across the iFrame boundary — only primitives and Object Sets are supported. Because the widget runs at a separate URL, OSDK client initialization requires that the Foundry hostname be derived from window.location.origin rather than a hardcoded string, since the deployment URL varies across Foundry stacks.| Limitation | Impact | Workaround |
|---|---|---|
| 10,000 object cap on Object Sets | Widget silently receives truncated Object Set | Migrate to Custom Widget if larger sets are needed |
| No struct variable support | Struct-typed Workshop variables cannot be passed in | Serialize struct fields as separate string parameters or JSON string |
| Separate URL deployment | CORS and auth must be configured for the Foundry stack domain | Use window.location.origin for OSDK client base URL |
| postMessage bridge latency | Parameter updates have small async delay vs native Workshop | Debounce rapid parameter changes in Workshop bindings |
| No Workshop Action integration | Cannot directly invoke Actions from iFrame context | Route Action calls through OSDK client using user token from context |
import React from 'react';
import {
WorkshopContextProvider,
useWorkshopContext,
} from '@osdk/workshop-iframe-custom-widget';
import { createClient } from '@osdk/client';
const client = createClient({
url: window.location.origin,
clientId: 'my-widget-client-id',
ontologyRid: 'ri.ontology.main.ontology.abc123',
});
function EmployeePanel() {
const context = useWorkshopContext();
const employees = context.fieldValues.employeeSet;
const statusFilter = context.fieldValues.statusFilter ?? 'all';
const handleEmployeeSelect = (employeeRid: string) => {
context.executeEvent('employeeSelected', employeeRid);
};
if (!employees) {
return <div>No employees bound.</div>;
}
return (
<div>
<p>Loaded {employees.objects.length} employees</p>
{employees.objects.map((emp) => (
<button key={emp.$primaryKey} onClick={() => handleEmployeeSelect(emp.$primaryKey)}>
{emp.fullName}
</button>
))}
</div>
);
}
export function App() {
return (
<WorkshopContextProvider>
<EmployeePanel />
</WorkshopContextProvider>
);
}⚠️ OSDK client auth failure
Hardcoding the Foundry hostname in the OSDK client initialization (e.g., `url: "https://my-company.palantirfoundry.com"`) will cause auth failures when the widget is deployed to a different Foundry stack, a staging environment, or behind a custom domain. Always derive the base URL from `window.location.origin` — because the iFrame is served from the Foundry stack itself, `window.location.origin` is always the correct Foundry base URL regardless of environment.
Deployment
npx @osdk/cli@latest widgetset deploy with a foundry.config.json at the repo root that specifies the widget RID, stack URL, and authentication token. This can be run locally by any developer with appropriate permissions and is suitable for teams without CI infrastructure.
CI/CD automation wraps the CLI invocation in a pipeline (GitHub Actions, GitLab CI, etc.) triggered on merge to the main branch. A service token stored as a CI secret is passed to the CLI. This is the recommended approach for production widgets — it ensures every merge to main produces a deployed artifact without manual intervention.
The Foundry CSP is the single most common deployment failure: all JavaScript, CSS, fonts, and images must be bundled into the zip. No requests to fonts.googleapis.com, cdn.jsdelivr.net, or any third-party host will succeed. Webpack/Vite configs must be set to bundle all assets locally.| Method | When to Use | Command/Steps |
|---|---|---|
| Manual zip upload | Initial setup, infrequent one-off deploys | 1. Build widget. 2. Zip output dir. 3. Upload via Widget Registry UI. |
| CLI (@osdk/cli) | Developer-initiated deploys, teams without CI | npx @osdk/cli@latest widgetset deploy --foundry-url <url> --token <token> |
| CI/CD automation | Production widgets, frequent releases, team environments | Pipeline runs CLI on merge to main using stored service token secret |
name: Deploy Custom Widget
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build widget
run: npm run build
- name: Deploy to Foundry
env:
FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_SERVICE_TOKEN }}
FOUNDRY_URL: ${{ secrets.FOUNDRY_STACK_URL }}
run: |
npx @osdk/cli@latest widgetset deploy \
--foundry-url "$FOUNDRY_URL" \
--token "$FOUNDRY_TOKEN" \
--widget-set-rid ri.widgetregistry.main.widget-set.abc123⚠️ CSP blocking external resources
A widget that works perfectly in local development will show a blank white screen after deployment to Foundry if it loads any resource from an external origin. Foundry's Content Security Policy blocks all external script, style, font, and fetch requests. Common culprits: Google Fonts imported via a `<link>` tag or `@import` in CSS, icon libraries loaded from a CDN, analytics scripts, and API calls to third-party services. Fix: configure your bundler to inline all fonts (base64 or self-hosted), import icon SVGs directly, and proxy any required external API calls through a Foundry Function.
Decision Trees
Which custom UI approach should you use?
Does your app need to integrate with Workshop (variables, events)?
Knowledge Check
Test your understanding with 3 questions. You need 2/3 to pass.