Build Apps · Start here
Every screen, data flow, integration and data model you create in Fuuz runs on the same stack: MongoDB, GraphQL APIs, the Data Flow Engine, Fuuz JSONata bindings and a deploy-anywhere infrastructure, with real-time updates over WebSockets. Knowing how each layer works, and how the layers talk to each other, is how you write better screens, faster flows and cleaner integrations, and how you find problems in minutes instead of hours.
This guide is written for engineers coming from SQL-backed, relational or legacy ERP and MES environments.
| Layer | What it is |
|---|---|
| Client layer | Screen Designer screens, mobile, external apps and IoT devices |
| Data Flow Engine | The low-code and pro-code workflow engine: web, backend and gateway flows |
| Transformation layer | Fuuz JSONata bindings and JavaScript: $state, $components, $metadata |
| GraphQL APIs | Multiple typed, schema-validated APIs |
| MongoDB | A JSON-native, multi-tenant document store with indexed collections |
| Infrastructure | Deploy-anywhere: auto-scaling, CDN, identity and access management, high availability and managed operations |
How the layers talk to each other: MongoDB reads and writes BSON documents; GraphQL serves typed queries, mutations and predicates; flows and screens reshape results with JSONata or JavaScript; and real-time events arrive over WebSockets.
_aggregate queries, not in JSONata transforms.$components, $metadata, $state and join functions.Tip: The stack gives you a clear way to isolate problems. Data wrong? Check the document shape with a GraphQL query. Query returning unexpected results? Check the predicates and filters. Transform producing bad output? Check the JSONata or JavaScript expression in the transform console. Screen not rendering? Inspect the element binding.
Live screen refresh, push events and shop-floor alerts are delivered by a dedicated WebSocket subscription service, separate from the GraphQL APIs. The result is the same, live data without polling, but it is a distinct service, not a GraphQL subscription.
This is the biggest shift for engineers from SQL-backed environments. MongoDB stores JSON documents, not rows, and how you design documents affects query performance, screen bindings and flow logic.
| Concept | SQL world | MongoDB and Fuuz |
|---|---|---|
| Data unit | Row in a table | JSON document in a collection |
| Relationships | Foreign keys and joins | Nested sub-documents or ID references |
| Schema changes | ALTER TABLE, a migration and risk | Add the field in the schema designer; it is live with near-zero downtime |
| Optional data | A nullable column on every row | The field is simply absent when it does not apply |
| Variable shapes | EAV tables or wide nullable rows | Each document carries its own fields |
| Filtering and aggregation | SQL WHERE and GROUP BY | GraphQL predicates and _aggregate queries at the API layer |
| Record ID | Primary key (varies) | Always id in Fuuz GraphQL, with no underscore |
In a relational MES or ERP, reconstructing one work order can take four tables and three joins, and adding a field means ALTER TABLE, a migration and a deployment window:
SELECT wo.id, wo.number, wo.status, p.name, p.revision, op.seq, op.description
FROM work_orders wo
JOIN parts p ON p.id = wo.part_id
JOIN wo_operations op ON op.wo_id = wo.id
WHERE wo.id = 'WO-1042';
In Fuuz, the whole object comes back in one read, and a new field such as customer_po is added in the schema designer:
{
"id": "WO-1042",
"number": "WO-1042",
"status": "In Progress",
"part": { "id": "part-ref-id", "name": "Bracket Assy", "revision": "C" },
"operations": [],
"customer_po": "PO-8812"
}
Because adding a field takes near-zero downtime, you can extend your applications without losing valuable production time.
The embed-versus-reference pattern is the right way to think about a document database. Today, Fuuz supports JSON schema and JSON objects within data models, and enumerations are handled by joining models, which keeps them clean and queryable. Full embedded document support is on the roadmap for early 2027. Apply the principles in your data models now, and they will be well placed as that support arrives.
Important: The most common mistake from SQL backgrounds is over-normalizing: creating a separate model for every sub-entity. Before you split, ask whether the data will ever be queried on its own, or whether it always travels with its parent. Fewer models often mean simpler queries, cleaner flows and easier screen bindings.
Fuuz exposes multiple GraphQL APIs. This is where you declare the data you want, apply filters and predicates, and run aggregations. When you create or extend a data model, its GraphQL API is generated and updated automatically.
edges { node { ... } }. Your data lives on node.orderBy: [{ field: "fieldName", direction: "asc" }], not sort._aggregate { id { count } } together inside edges.node. There is no separate groupBy argument.id, never _id.where variables. Never fetch everything and filter in JSONata.Good habits:
_aggregate runs on the server, which is far more efficient than counting in JSONata.id from mutations. It confirms the write and gives you the ID for chained operations.The GraphQL editor autocompletes and validates against the schema as you type, so field-name errors and type mismatches are caught before runtime.
query GetOpenWOs($where: WorkOrderWhereInput, $orderBy: [WorkOrderOrderByInput!], $first: Int, $after: String) {
workOrder(where: $where, orderBy: $orderBy, first: $first, after: $after) {
pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
total
edges {
node {
id
number
status
scheduledStart
part { name revision }
}
}
}
}
{
"where": { "workCenterId": { "_eq": "wc-123" }, "status": { "_eq": "Open" } },
"orderBy": [{ "field": "scheduledStart", "direction": "asc" }]
}
mutation UpdateWOStatus($id: ID!, $status: String!, $completedAt: DateTime) {
updateWorkOrder(id: $id, data: { status: $status, completedAt: $completedAt }) {
id
status
completedAt
}
}
The group field goes on the node, with _aggregate beside it. Each edge.node in the result contains the group field and its count.
query WOStatusCounts($where: WorkOrderWhereInput, $orderBy: [WorkOrderOrderByInput!]) {
workOrder(where: $where, orderBy: $orderBy) {
total
edges {
node {
workOrderStatusId
_aggregate { id { count } }
}
}
}
}
See also Aggregate Query API.
Do not fetch every record and filter in a JSONata transform such as $[status = "Open"]. Filter in the query instead:
query {
workOrders(where: { status: { _eq: "Open" } }) {
id
status
number
}
}
The fastest way to learn Fuuz GraphQL is to let the built-in tools write it for you. Go to System > Business Intelligence > API Explorer in any tenant. The Query Builder is under Tools at the top of the editor, and everything runs against your tenant's schema in real time.
$where, $orderBy, $first and $after, with their exact types.Do this a dozen times across different models and Fuuz GraphQL will feel natural.
The Data Flow Engine sits above the GraphQL layer. It is your application's business logic layer: built visually, with the option to drop into JavaScript for complex operations. Flows call GraphQL APIs to read and write data, then use the transformation layer to process the results. Keep that order in mind when you trace what triggered what.
| Flow type | What it does |
|---|---|
| Web flows | Triggered by user interactions on screens. They also act as remote function calls (RFC) that any external system can call. Web flows with screen context can use $components and screen elements directly. |
| Backend flows | Server-side logic triggered by events, schedules or API calls: integrations, data processing, ERP write-backs and anything that should not run on the client. |
| Gateway flows | Expose Fuuz logic to external systems as callable endpoints, so they can push data in or trigger operations. |
Standard JSONata is the foundation; Fuuz extends it with more than 200 platform bindings for screen state, user identity, flow context, joins and more. For the standard language, see the JSONata tutorial and the custom Fuuz-only JSONata library.
$components, $metadata, $card, data and $$.$state, $state.context and $state.claims. Web flows that run with screen context can also use $components and related screen bindings.| Expression | What it does |
|---|---|
$components.Form1.fn.save() |
Save the form |
$components.Form1.fn.setValue("field", val) |
Set one field value |
$components.Form1.formState.dirty |
True when there are unsaved changes |
$components.Form1.formState.valid |
True when all validations pass |
$components.Form1.data.fieldName |
Read a field value |
$components.Table1.fn.search() |
Refresh the table |
$components.Table1.selectedRows |
All selected rows |
$components.Table1.selectedRows[0].id |
The first selected row's ID |
$components.Screen.fn.setContextValue("k", v) |
Set one screen context key |
$components.Screen.fn.mergeContext({...}) |
Shallow-merge values into the screen context |
$components.Screen.context.myKey |
Read a screen context value |
| Expression | What it does |
|---|---|
$metadata.urlParameters.id |
URL path parameter, such as /record/:id |
$metadata.querystring.tab |
Query string parameter, such as ?tab=details |
$metadata.user.id |
Signed-in user's ID |
$metadata.user.email |
Signed-in user's email |
$metadata.user.roles |
The user's roles |
$metadata.tenant.id |
Current tenant ID |
$metadata.tenant.name |
Current tenant name |
$metadata.settings.timezone |
The user's time zone |
$metadata.settings.dateFormat |
The user's date format |
| Expression | What it does |
|---|---|
$ |
The current node payload, shorthand for $state.payload |
$state.payload |
The full input payload of the current node |
$state.context |
Shared flow context, set with Set Context or Merge Context nodes |
$state.context.myKey |
Read one context value |
$state.claims.userId |
The triggering user's ID |
$state.claims.tenantId |
Tenant ID from the auth claims |
$state.claims.roles |
The user's roles from the auth claims |
$state.metadata.flowId |
The current flow's ID |
$state.lastError |
The error from the last caught error node |
| Expression | What it does |
|---|---|
data.fieldName |
Read a field value inside a form element |
data.active ? "Yes" : "No" |
A condition on a boolean form field |
$card.data.id |
The card record's ID inside a cards element (not data.id) |
$card.data.status |
A field value on the card record |
$$.newValue |
The new value in an onChange handler |
$$.oldValue |
The previous value in an onChange handler |
$isNotNilOrEmpty($$.newValue) and $not($$.newValue = $$.oldValue) |
Fire only when the value is not empty and actually changed |
/* $innerJoin: match records from both sides */
$innerJoin(workOrders, parts, "partId", "id")
/* $leftOuterJoin: keep every left record, match the right */
$leftOuterJoin(workOrders, scheduledShifts, "shiftId", "id")
/* $query: run a GraphQL query inline */
$query({ "api": "system", "statement": "query { ... }", "variables": { "id": "123" } })
/* $numeral: number formatting */
$numeral(12345).format("0,0") /* "12,345" */
$numeral(0.75).format("0%") /* "75%" */
$numeral(1234.5).format("$0,0.00") /* "$1,234.50" */
$now /* ISO 8601 timestamp */
$appConfig.someKey /* tenant app configuration */
| Gotcha | Correct usage |
|---|---|
No ! operator |
Use $not(expr) |
String concatenation is & |
"a" & " " & "b", not + |
Equality is = |
x = "val", not == or === |
| Membership | x in [1,2,3], not .includes() |
No ?? operator |
$exists(val) ? val : "default" |
| Cards versus forms | Inside cards, use $card.data.x, not data.x |
Context is $ |
$ is the whole current input; confirm its shape in the console before writing transforms |
These are the closest useful parallels, not a one-to-one mapping.
| SQL and relational | Fuuz |
|---|---|
| Normalize everything and rebuild the object with joins at query time. | Design for your access pattern. Data that always travels together belongs together. |
| Schema changes are risky: ALTER TABLE, migrations, deployment windows and table locks. | Add a field in the schema designer with near-zero downtime. Field changes are low-risk, fast tasks. |
| Full embedded documents are expected in document databases. | Fuuz supports JSON schema and JSON objects in data models today, with full embedded document support on the roadmap for early 2027. Enumerations are joins between models. |
| You need a database diagram and documentation just to start a query. | Go straight to the Query Builder, where every relationship is a click away, and use the Application Graph to see how models, screens and flows connect. |
| Filter and aggregate with WHERE and GROUP BY. | Filter with where variables, aggregate with edges.node.fieldName and _aggregate, and sort with orderBy. JSONata reshapes; it does not filter. |
| Combine tables with INNER JOIN and LEFT JOIN in the database. | Combine results from several GraphQL queries in a flow transform with $innerJoin and $leftOuterJoin. |
| Backups, upgrades, scaling and disaster recovery are your team's job. | In Fuuz-hosted environments, infrastructure is managed and your focus is application delivery. Application performance on large datasets is still a design responsibility. |
The Application Designer includes the Application Graph: a live, visual knowledge graph of your application that shows every relationship between data models, screens and data flows in one interactive view. It reflects your actual application, not a static diagram.
where, count and sum with _aggregate, sort with orderBy, and use JSONata only to reshape the filtered response.id in mutations. It confirms the write and feeds chained operations.$components, $metadata, data and $card; flows use $state. Mixing them up fails silently.The build loop: confirm the document shape in the schema designer, write the GraphQL query with predicates, reshape the result with JSONata, bind it to the screen element or flow output, then inspect each layer in the console and logs before calling it done.
Start at the data layer and work up, every time.
| Symptom | Suspect layer | Where to look |
|---|---|---|
| Screen shows no data | GraphQL query or predicate | Run the query on its own. Does the predicate return records? Is the field name right? |
| Wrong data on screen | JSONata binding or wrong field | Check the raw GraphQL response first. If it is right, check the JSONata expression in the transform console. |
| Screen loads slowly | Dynamic expressions or missing indexes | Usually dynamic bindings, such as visibility expressions, running too often, or complex filters on large collections without an index. |
| Flow not triggering | Trigger configuration | Confirm the trigger event matches the action, and check the flow execution logs. |
| Flow fails mid-run | A specific node | Open the flow execution history to find the failing node, and inspect the input it received in the console. |
| Mutation fails | Field name or type mismatch | Read the error; GraphQL errors are explicit. Remember id, not _id. |
| Integration writes missing | Mutation not running or wrong tenant | Confirm the mutation ran in the execution logs, check the tenant context and check the id returned. |
| JSONata returns null | Wrong context or field path | Are you in a screen or a flow? $state works only in flows, $components and $metadata only in screens, and cards use $card.data.x. |
The debug order:
Skipping steps leads to false diagnoses.
When Fuuz hosts your environment, it changes what belongs in your delivery scope.
At a glance: adding a field to a model takes near-zero downtime, and one GraphQL call returns a fully hydrated object graph.