Building Blocks
| Type | Use Case | Example |
|---|---|---|
| string | Store text input values, filter keywords, status labels | "active" |
| number | Store numeric thresholds, counts, slider values | 42 |
| boolean | Toggle visibility, enable/disable widgets, flag state | true |
| date | Store calendar date selections without time component | "2024-06-15" |
| timestamp | Store precise point-in-time values including time zone | "2024-06-15T09:30:00Z" |
| objectSet | Hold a filtered or full set of Ontology objects for table/chart input | ObjectSet<Order> |
| object | Hold a single selected Ontology object (e.g., from a row-click) | Order { id: "ORD-001" } |
| struct | Hold a composite value with named fields not backed by an object type | { lat: 37.7, lon: -122.4 } |
| stringList | Hold a list of string values for multi-select inputs or loop sources | ["red", "green", "blue"] |
| numberList | Hold a list of numeric values for chart series or batch inputs | [10, 20, 30] |
| booleanList | Hold per-row toggle states in a loop layout | [true, false, true] |
{
"eventId": "onSubmitResolution",
"trigger": "onClick",
"widgetId": "resolveButton",
"condition": {
"type": "variableEquals",
"variable": "selectedStatus",
"value": "open"
},
"actions": [
{
"type": "invokeActionType",
"actionTypeApiName": "resolveIncident",
"parameters": {
"incidentId": { "fromVariable": "selectedIncident.id" },
"resolutionNote": { "fromVariable": "resolutionNoteInput" },
"resolvedAt": { "fromVariable": "currentTimestamp" }
}
},
{
"type": "setVariable",
"variable": "selectedIncident",
"value": null
},
{
"type": "closeOverlay",
"overlayId": "incidentDetailOverlay"
}
]
}⚠️ Too many variables on initial load
Every variable with a non-null default value that is backed by an object set query executes that query when the page loads, even if the widget consuming it is below the fold or hidden behind an overlay. On data-dense applications with 10+ object set variables, this causes a cascade of parallel Ontology queries at load time, making the page feel slow for all users regardless of what they actually need. Audit your variables and mark any that are not required for the initial page view as lazy-loaded — Workshop will defer their queries until the first widget that binds them becomes visible or their triggering event fires.
Widget Patterns
| Widget | Purpose | Key Config |
|---|---|---|
| Object Table | Display and select from a list of Ontology objects | objectSet binding, column definitions, onRowClick event |
| Object Set Filter | Let users filter the bound objectSet by property values | target objectSet variable, filter property, filter UI type |
| Chart (bar/line/pie) | Aggregate and visualize object properties as charts | objectSet binding, X axis property, Y axis aggregation function |
| Map | Render objects with geospatial properties on an interactive map | objectSet binding, geohash or lat/lon property, marker style |
| Button | Trigger events including Action Type invocations | label, onClick event chain, visibility condition |
| Text Input | Capture free-text from the user and store in a string variable | bound string variable, placeholder, onChange event |
| Metric Card | Display a single aggregated KPI value with optional trend | objectSet binding, aggregation (count/sum/avg), display format |
{
"widgetId": "incidentTable",
"type": "objectTable",
"bindings": {
"objectSet": { "fromVariable": "filteredIncidents" },
"onRowClick": {
"actions": [
{
"type": "setVariable",
"variable": "selectedIncident",
"value": { "fromEvent": "clickedObject" }
},
{
"type": "openOverlay",
"overlayId": "incidentDetailOverlay"
}
]
}
},
"columns": [
{ "property": "incidentId", "displayName": "ID", "width": 100 },
{ "property": "title", "displayName": "Title", "width": 300 },
{ "property": "severity", "displayName": "Severity", "width": 120 },
{ "property": "status", "displayName": "Status", "width": 120 },
{ "property": "assignee", "displayName": "Assignee", "width": 160 },
{
"type": "functionBacked",
"functionApiName": "computeResolutionEta",
"displayName": "ETA",
"width": 140,
"parameters": { "incidentId": { "fromProperty": "incidentId" } }
}
]
}⚠️ Function-backed columns
Function-backed columns in an Object Table invoke a Foundry Function once per row on every render cycle. On a table displaying 500 rows, that is 500 separate function executions fired in parallel on each page load and each filter change. Function latency is multiplied by row count — a 200ms function becomes a 200ms-per-row cost paid 500 times. For large tables, move computed properties into the upstream Transform pipeline as pre-computed dataset columns, expose them as standard Object Type properties, and use property columns instead. Reserve function-backed columns only for values that genuinely cannot be computed at pipeline time (e.g., real-time external lookups).
Event System
| Category | Triggers When | Common Use |
|---|---|---|
| onClick | User clicks a button, table row, map marker, or any clickable widget | Invoke an Action Type, open an overlay, navigate to a page |
| onSelect | User selects an option from a dropdown, radio group, or multi-select | Set a filter variable, update a chart grouping, populate a form field |
| onChange | User modifies a text input, slider, date picker, or toggle | Update a search filter variable, recompute a derived variable in real time |
| onSubmit | User submits a form container widget | Validate inputs then invoke a CREATE or EDIT Action Type |
| onLoad | A page or overlay finishes rendering for the first time | Initialize variable defaults, load user-context data, log a page view |
| onFilter | An Object Set Filter widget applies or removes a filter predicate | Sync the filtered objectSet variable to downstream tables and charts |
{
"eventId": "onSearchInputChange",
"trigger": "onChange",
"widgetId": "searchInput",
"actions": [
{
"type": "setVariable",
"variable": "searchKeyword",
"value": { "fromEvent": "currentValue" }
},
{
"type": "setVariable",
"variable": "filteredIncidents",
"value": {
"type": "objectSetFilter",
"baseObjectSet": "allIncidents",
"filter": {
"property": "title",
"operator": "contains",
"value": { "fromVariable": "searchKeyword" }
}
}
}
]
},
{
"widgetId": "incidentTable",
"bindings": {
"objectSet": { "fromVariable": "filteredIncidents" }
}
},
{
"widgetId": "severityChart",
"bindings": {
"objectSet": { "fromVariable": "filteredIncidents" }
}
}⚠️ Circular event chains
Circular event chains occur when Event A sets Variable X, a widget bound to Variable X fires Event B which sets Variable Y, and a widget bound to Variable Y fires Event A again. Workshop does not automatically detect or break these cycles — the application enters an infinite re-render loop that degrades to an unresponsive page and generates an unbounded number of Ontology queries. The fix is to identify the cycle in the event graph and break it with a guard condition: add a condition to one of the event steps that checks whether the target variable already holds the value about to be written, and skips the setVariable if so. Alternatively, restructure the data flow so the downstream variable derives its value reactively rather than through a manual event chain.
Common Application Patterns
| Pattern | Components | Best For |
|---|---|---|
| Inbox / Task Management | Object Table (filtered by status) + detail overlay + Action button | Incident response, work order queues, approval workflows, support tickets |
| Common Operational Picture (Map-centric) | Map widget + Metric Cards + Charts + Object Set Filters | Fleet tracking, field operations, geographic asset monitoring |
| Common Operational Picture (Dashboard) | Metric Cards + Charts + Object Set Filters + tabbed layout | Executive dashboards, KPI monitoring, operational health views without geo context |
| Investigation Workflow | Left-panel Object Table + right-panel detail section + linked object sub-tables | Supply chain tracing, fraud investigation, entity relationship exploration |
| What-If Scenario Comparison | Scenario widgets + parameter inputs + side-by-side output charts and metrics | Logistics optimization, demand planning, resource allocation modeling |
⚠️ Overloading a single page
It is tempting to build all application functionality into a single Workshop page, using overlays and conditional visibility to reveal and hide sections. This approach quickly becomes unmanageable: the variable model grows large, event chains become difficult to trace, and page load time increases as every object set variable queries on render. Split complex workflows across multiple pages with a clear navigational model. Use shared variables sparingly — only for state that genuinely needs to survive page transitions (e.g., a selected entity that the user is working on across pages). State that is local to a workflow step should live on the page where that step occurs, not in global variables.
Performance
| Issue | Symptom | Fix |
|---|---|---|
| Unfiltered object set passed to a widget | Page loads slowly; chart or table takes 5+ seconds to render on initial open | Add filter expressions or Object Set Filter widgets to narrow the set before it reaches the widget |
| Function-backed columns on large tables | Table renders slowly; Performance Profiler shows function calls per row | Pre-compute the value in the upstream Transform pipeline and expose it as an Object Type property |
| Too many variables loading on initial render | Profiler waterfall shows 10+ parallel object set queries on page load | Set non-critical variables to lazy load; trigger them from the event that reveals their consuming widget |
| Pagination disabled on large tables | Table with 1000+ rows takes a long time to display; browser memory usage is high | Enable server-side pagination; set a reasonable page size (25–100 rows) |
| Stale data after Action Type invocation | Table still shows the old value after a successful action | Ensure the modified object type is listed in the action's output schema so Workshop invalidates the cache |
| Duplicate object set queries across widgets | Profiler shows identical queries fired by multiple widgets independently | Bind both widgets to a single shared objectSet variable instead of each computing their own |
⚠️ Passing unfiltered object sets to charts
Chart widgets in Workshop perform aggregation at query time against the bound objectSet. When the bound objectSet is a base set with no filters — for example, all Orders across all time — the Ontology must scan and aggregate every object in that type, which can be tens of millions of rows. This produces chart load times measured in tens of seconds and places heavy load on the Ontology query engine for every user who views the page. Always pre-filter the objectSet to the minimum required scope before binding it to a chart: filter by time range, region, status, or any other dimension that is logically relevant to the chart's purpose. If the chart genuinely requires an all-time aggregate, pre-compute it as a pipeline-built dataset metric and surface it through a Metric Card instead.
Decision Trees
Which Workshop app pattern fits your use case?
What is the primary user action?
Knowledge Check
Test your understanding with 3 questions. You need 2/3 to pass.