Prerequisites
- Comfortable editing JSON and HTML files
- Basic JavaScript knowledge is helpful but not required
- Access to a Synkronus server and the Formulus app
- (Optional but recommended) A running Synkronus server set up using the Synkronus Quickstart repo
The custom_app is where your forms (questionnaires) live and where you add any custom application logic – things like longitudinal data collection, dashboards and summaries, helper tools for data collectors, and much more. In short: this is where you turn "just forms" into a real app.
A custom_app is a zipped archive containing two folders at the root level:
|
|- app/
|_ forms/Forms are specified using the jsonforms format, extended with some ODE-specific question types for collecting data such as photos, signatures, QR codes, GPS coordinates, etc. You can even add your own question types (more on that in a later article).
Each form is placed in a subfolder ./forms/<form_name>/ and contains:
schema.json– the shape of the data to be collectedui.json– how the flow and layout of the questions should look
Together, these two files fully describe a questionnaire.
The app part of the custom_app bundle is where you build richer data collection experiences around your forms. You can:
- Look up existing observations (a filled-out form is called an
observation) - Open new forms and inject pre-defined or dynamic values into them
- Show dashboards, summaries, helper text, instructions, and more
The app is built with HTML and JavaScript (including support for frontend frameworks such as SolidJS, React, Angular, etc.), so if you can build a web page, you can build a custom_app.
To make this concrete, we will walk through building a small demo app: Coffee Tracker. It is intentionally simple so you can focus on how things fit together and then adapt it to your own use case.
For this first demo we will create a small form to collect information about roasted coffee beans. We will capture:
- a photo of the beans
- name of the coffee variety
- origin country
- who roasted the coffee
- the roasting date, and
- roast level
That is all for now – enough to be useful without getting in the way.
Our goal is to create the two jsonforms files that specify this form.
Formulus currently supports a subset of the full jsonforms spec, and adds its own control properties and question types (for example: photo, QR scanner, etc.).
We will extend our app later in version 2.0 (see this guide). For version 1.0 we will keep things deliberately simple: a single HTML page with a nice image of the ODE mascot sipping coffee
(fun, but irrelevant, fact: the mascot's name is Girrna).
To get started, create this folder structure for our Coffee Tracker:
|
|- app/
| |- index.html
| |_ coffee_cup.png
|_ forms/
|_ register_coffee/
|- schema.json
|_ ui.jsonWe will then zip this into a file called coffee_tracker-v1.0.0.zip and upload it to the Synkronus portal.
When we say "forms", we mean the specifications for the questionnaires. Each form specification has two parts:
- one defining the shape of the data collected (
schema.json) - one defining the UI and flow (
ui.json)
To create the jsonforms specification, we have several options. The jsonforms.io community provides tooling for form authoring, but since our form is small, we will define it by hand.
We also strongly encourage using AI/LLMs for form authoring – the JSON format is text-based and well-defined, which makes it easy for an LLM to work with. The ODE developers are working on richer contexts (skills) that can be used with AI agents to make this even smoother.
Below we define the shape of our registration data. We will have 5 text values and one photo, and we’ll also mark a couple of fields as required.
{
"type": "object",
"properties": {
"photo": {
"type": "object",
"format": "photo",
"title": "Bean photo",
"description": "Take a picture of the beans in portrait mode"
},
"name": {
"type": "string",
"title": "Bean variety",
"description": "Name of the coffee variety"
},
"origin": {
"type": "string",
"title": "Country of Origin"
},
"roaster": {
"type": "string",
"title": "Roaster",
"description": "Who roasted the coffee"
},
"roast_date": {
"type": "string",
"format": "date-time",
"title": "Roasting date",
"description": "Date and time when this batch was roasted"
},
"roast_profile": {
"type": "string",
"title": "Roast Profile",
"enum": ["dark", "medium", "medium-light", "light"]
}
},
"required": [
"name",
"roast_date"
]
}Save this file as forms/register_coffee/schema.json.
Next we define how the form should be displayed in ui.json – that is, which questions appear on which screens and in what order.
In Formulus:
- A "screen" is defined as a
SwipeLayoutelement. - If we want multiple questions on the same screen, we can use a
VerticalLayoutto group them. - We can add labels, special properties (e.g. autocomplete), and more.
For now we will keep it simple and just add a header label and the controls.
{
"type": "SwipeLayout",
"options": {
"headerTitle": "Register Coffee",
"headerFields": ["name"]
},
"elements": [
{
"type": "Label",
"text": "<h2 style='background: #9C2C07; padding: 16px; border-radius: 8px; margin-bottom: 16px; color: cream'>Mmmm... Coffee...</h2>",
"options": {
"html": true
}
},
{
"type": "Control",
"scope": "#/properties/photo"
},
{
"type": "Control",
"scope": "#/properties/name"
},
{
"type": "Control",
"scope": "#/properties/origin"
},
{
"type": "Control",
"scope": "#/properties/roaster"
},
{
"type": "Control",
"scope": "#/properties/roast_profile"
},
{
"type": "Control",
"scope": "#/properties/roast_date"
}
]
}Save this file as forms/register_coffee/ui.json.
For the application part, version 1.0 will just be a static page without any extra functionality.
Create an HTML file and place it, together with any other files like images or stylesheets, in the app folder.
For example:
<!DOCTYPE html>
<html>
<head>
<title>ODE Demo App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>ODE Demo App</h1>
<h2>v1.0</h2>
<img src="progression_1.png">
</body>
</html>Save this file as app/index.html together with a CSS stylesheet and the image.
When all that is done, the folder structure should look like this:
Then it is just a matter of creating a zip file containing the forms and app folders at the root level. The structure of the zip file should be like this:
The custom_app can be uploaded by logging into the Synkronus portal and going to the App Bundle page.
Alternatively, you can use the CLI (command line interface).
The CLI executable can be downloaded from our GitHub releases page – select the appropriate version for your OS.
(We are working on distributing the CLI via Homebrew, WinGet, etc., but for now please bear with us and install it manually by downloading the relevant .exe file.)
./synk app-bundle upload /path/to/the/zipped/demo_app_v1.0.0.zip -aTo use the app:
- Install Formulus on a device
- Configure the server settings and log in
- Go to the Sync page and click Update App Bundle
You should now see your Coffee Tracker in action:
Read the next article in this series to see how to implement an app that can do smooth longitudinal data collection tailored exactly to your needs –
... once you've had a cup of coffee, of course!
- CI/CD setup to automatically update a custom_app
- Shared choice lists / dynamic lists
- Custom question types


