At JTL Academy Day in Cologne, we wanted to answer a simple question: What can you actually build with JTL Cloud Apps?
We didn't want to answer that with a product demo or a hypothetical architecture. We wanted to start with real business ideas and turn them into working apps inside Cloud ERP.
During a 90-minute session titled "Kein Entwickler? Kein Problem!" (No developer? No problem!), the audience called out app ideas while we used Claude Code to build them. We registered the apps, opened them inside Cloud ERP, and tested them live.
Across the two conference days, six apps came out of that experiment:
- A dashboard that suggests which products to reorder
- A Panel that recognizes a customer's first order and drafts a welcome email
- A customer returns-risk score
- A weather-based sales and reorder tool for sports shops
- A tool that calculates the actual purchase value of goods sold
- A margin dashboard that breaks results down by supplier, customer, and delivery address
The source code, screenshots, and prompt histories are available in the academy-day-examples repository.
These aren't production applications. We deliberately kept them small so we could build and test them during the session. But that also makes them useful examples of how different parts of the JTL Cloud Platform fit together, from dashboards and Panels to ERP API access, backend services, external data, data aggregation, and different backend languages.
More importantly, they show that a JTL Cloud App doesn't have to be another generic dashboard sitting beside your ERP. It can become part of the workflow your customers already use.
The Six Apps at a Glance
Each app started with a business problem, not a technical requirement.
| App | What it does | Where it appears | Key pattern |
| Bestandsradar | Suggests which items to reorder and when | Cloud ERP menu | Dashboard with backend |
| first-order-welcome | Recognizes a customer's first order and drafts a welcome email | Sales order Panel | Context-aware Panel |
| Kundenrisiko | Scores a customer's returns risk | Customer Panel + Cloud ERP menu | Panel with .NET backend |
| Wetter-Sport | Combines sales history with weather data to suggest reorders | Item Panel + Cloud ERP menu | ERP data + external API |
| hand-build-app | Calculates the actual purchase value of goods sold | Customer Panel + Cloud ERP menu | Order data aggregation |
| Profit Pulse | Breaks revenue and gross margin down by supplier, customer, and delivery address | Cloud ERP menu | Financial data aggregation |
The interesting part isn't just what each app does. It's how different the ideas are while using the same JTL Cloud App building blocks.
Some apps live in the Cloud ERP menu, while others appear alongside a customer, item, or sales order. Some work with JTL data alone; others combine it with external APIs or process larger amounts of data in a backend service.
The platform provides the building blocks. The business problem determines what you build with them.
Every App Starts the Same Way
Five of the six apps started from the official JTL Cloud App template. This gives developers a consistent starting point, whether they're building a dashboard, a Panel, or an app that needs a backend service.
The basic workflow looks like this:
-
Create the app with
npx @jtl-software/create-cloud-app@latest. -
Register it with
npm run register. - Install it from the JTL Hub under Manage apps → Apps in development.
-
Run it locally with
npm run dev.
The template provides the project structure, frontend, optional backend, and manifest that describes the app to Cloud ERP. From there, you can adapt the structure to the problem you're solving rather than setting up the entire application from scratch.
The manifest is particularly important because it defines the app's footprint in the platform. For example, Bestandsradar defines a configuration page, a menu item, the API scopes it needs, and its authentication configuration:
{
"version": "1.0.0",
"technicalName": "bestandsradar-e354dd",
"lifecycle": {
"configurationUrl": "http://localhost:3024/setup"
},
"capabilities": {
"hub": {
"appLauncher": { "redirectUrl": "http://localhost:3024/hub" }
},
"erp": {
"menuItems": [
{ "id": "bestandsradar-menu", "name": "Bestandsradar", "url": "http://localhost:3024/erp" }
],
"api": {
"scopes": ["items.read", "salesorders.read"]
}
}
},
"authentication": {
"publicClient": {
"redirectUris": ["http://localhost:3024/callback"],
"postLogoutRedirectUris": ["http://localhost:3024/"]
},
"serviceAccount": {
"clientCredentials": true,
"description": "bestandsradar backend service account"
}
}
}
There isn't much platform-specific configuration hidden away elsewhere. The manifest tells JTL what the app needs and where it should appear.
-
configurationUrlis the setup page the JTL Hub opens during installation. -
menuItemsputs an entry in the Cloud ERP's Apps menu that loads/erpin an iframe. -
api.scopesis the data the app may read. Two scopes, both read-only, because a reorder dashboard needs items and sales orders and nothing else. -
serviceAccountlets the backend get its own access token with client credentials.
The localhost URLs are fine while you develop. Before anyone else installs the app, every URL in the manifest has to point to a real HTTPS host.
Pattern 1: Put a workflow behind a Cloud ERP menu item
Bestandsradar answers a question every merchant asks on Monday morning: what should I reorder, and by when? It opens from the Apps **section under Third-party Apps > Bestandsradar** in Cloud ERP and shows the best sellers, a list of items about to run out, and a suggested order quantity for each.
The interesting part isn't the dashboard itself. It's what happens behind it. The frontend gets an app token through AppBridge and passes it to the backend. The backend verifies the token, extracts the tenant from its claims, and uses its service account to query the ERP API.
That gives you a useful pattern for larger JTL Cloud Apps: ERP interface → app backend → JTL API
The backend can combine API calls, run business logic, or process larger amounts of data. Tenant handling matters here, too, because the backend should derive the tenant from the verified token rather than trust a value supplied by the browser.
Bestandsradar also keeps its reorder calculation separate from the API layer. That makes the logic easier to test and reuse, including in the demo route, which runs against sample data.
Pattern 2: Put context where the work happens
A dashboard is useful when someone wants to review information. But what if they’re already working on a customer, order, or item? That’s where a Panel comes in. It sits alongside the Cloud ERP’s own screens and shows relevant context for whatever the user has selected, so they can get the information they need without leaving the screen they’re working on.
Three apps use Panels to bring information into the existing Cloud ERP workflow:
- first-order-welcome looks at the sales order currently open.
- Kundenrisiko shows a risk score for the selected customer.
- Wetter-Sport connects an item to the weather conditions on the days it sold.
The user doesn't have to open another application or copy an ID somewhere else. The app follows the context they're already working in.
For example, a Panel can subscribe to changes in the selected customer:
useEffect(() => {
const unsubscribe = appBridge.event.subscribe(
'CustomerChanged',
(data: unknown) => {
setCustomerId((data as { customerId: string }).customerId);
}
);
appBridge.method
.call('getCurrentCustomerId')
.then((id) => setCustomerId(id as string))
.catch(() => undefined);
return () => unsubscribe();
}, [appBridge]);
The same idea applies to other contexts, such as sales orders and items. The Panel can retrieve the currently selected record and react when the user switches to another one.
This is one of the patterns worth taking beyond the demo. A JTL Cloud App doesn't always need to replace an existing ERP workflow. Sometimes the better experience is to add useful context to the existing workflow.
Pattern 3: Sometimes you don't need a backend
Not every app needs a server. The first-order-welcome Panel takes the app token from AppBridge and uses it directly to query the ERP GraphQL API:
const { accessToken } =
await appBridge.method.call<{ accessToken: string }>('getAppToken');
const client = new GraphQLClient(`${jtlApiUrl}/erp/v2/graphql`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
For a small Panel that reads a few records about the thing currently on screen, this can be a simpler architecture. There's no backend to host or additional server-side proxy between the Panel and Cloud ERP.
That doesn't mean frontend-only is always the right answer. A backend becomes useful when an app needs secrets, third-party API credentials, shared caching, or heavier processing.
Start with the simplest architecture that fits the problem, and add a backend when the problem needs one.
Pattern 4: ERP data becomes more useful when you combine it with other data
Wetter-Sport started with a simple observation from a sports shop owner: my sales depend on the weather.
An ERP contains sales history, products, stock, and orders. It doesn't know whether tomorrow will be sunny, so the app combines ERP data with weather information from Open-Meteo.
For an item, the Panel shows the weather on the days it sold. The dashboard uses historical sales and a seven-day forecast to suggest what might need to be reordered before the weekend. The app turns raw weather data into a few simple categories:
export function classify(
tempMax: number,
precipitation: number,
sunshineHours: number
): WeatherClass {
if (precipitation >= 1) return 'rain';
if (tempMax < 8) return 'cold';
if (tempMax >= 18 || (tempMax >= 14 && sunshineHours >= 6)) {
return 'sunny';
}
return 'mixed';
}
From there, the app compares sales behaviour across those weather classes and uses the forecast to estimate demand.
The same pattern could combine ERP data with shipping information, marketing data, exchange rates, public APIs, machine-learning models, or data from another system your customers already use. The Cloud App becomes the place where those sources meet.
There was also a useful lesson in what didn't work immediately. The weather archive had a delay for recent dates, so some of the latest sales initially had no weather data. The solution was to combine the archive with the forecast endpoint. That's the sort of edge case that tends to appear when an idea moves from a demo to something people rely on.
Pattern 5: Working with more data changes the problem
Two apps tackle money questions, which means adding up a lot of line items. hand-build-app calculates the actual purchase value of everything sold. profit-pulse breaks down revenue and gross margin by supplier, customer, and delivery address, with monthly trends and CSV export.
Both face the same challenge: totals live on line items, which you must fetch separately for each order or invoice. A hundred orders can mean a hundred requests. Send them all at once, and you risk hammering the API. Send them one at a time, and the dashboard takes forever to load.
Both apps use controlled concurrency when fetching data for many orders or invoices. profit-pulse, for example, keeps five requests running at a time rather than sending every request simultaneously:
async function parallelMap(values, limit, fn) {
const results = new Array(values.length);
let next = 0;
await Promise.all(
Array.from(
{ length: Math.min(limit, values.length) },
async () => {
while (next < values.length) {
const index = next++;
results[index] = await fn(values[index]);
}
}
)
);
return results;
}
const invoiceItems = await parallelMap(valid, 5, (invoice) =>
jtlGet(`/sales-invoices/${encodeURIComponent(invoice.salesInvoiceId)}/line-items`, context),
);
The lesson is broader than this particular implementation. A dashboard that works perfectly with ten orders can behave very differently with thousands, so think about concurrency, caching, and API limits early.
The same care applies to the numbers you're showing. hand-build-app calculates actual purchase value rather than using the item's average purchase price. Profit Pulse uses the item's current default supplier and excludes returns, shipping costs, and overheads from its gross margin calculation.
When an app shows financial information, the calculation needs to be as clear as the UI. Say what the number means, and what it doesn't mean.
Pattern 6: Your backend language is your choice
Kundenrisiko takes another approach. Its backend runs on .NET 8 rather than Node.js, but the frontend doesn't need to care. It sends a Bearer token and receives a JSON response.
The .NET implementation uses Microsoft's token validation libraries to retrieve the issuer configuration and signing keys, then validates the token before processing the request.
From the frontend's perspective, none of that changes. A JTL Cloud App can use Node.js, .NET, or PHP while keeping its application interface consistent.
What Building These Apps With AI Taught Us
The finished apps are only half of the story. Every app in the repository also keeps its prompt history, so you can see not just what was built, but how the AI assistant got there.
Give the assistant the platform context first. Kundenrisiko started with a request to build and register an app that scores customer return risk. The assistant initially took the wrong approach to registration. Follow-up prompts asked it to use the CLI and consult the JTL Developer Docs. Once the JTL Developer Docs MCP server was connected, the assistant had the context it needed to follow the official workflow. AI can make building faster, but it still needs the right context.
Review the manifest like you would review code. One prompt in hand-build-app asked the assistant to expand the application's scopes arbitrarily. It requested more permissions than the final app needed, and the manifest was later trimmed back. Review scopes and authentication configuration as carefully as you would any other code in a pull request.
Say whether you're building a demo or a real integration. Kundenrisiko initially used mock data. Asking for real data exposed a limitation: the GraphQL API didn't provide the general returns query the original risk model expected. Mock data can prove that a UI works, but it can't tell you whether the platform has the data your business logic needs. Ask the assistant to use real APIs early when building an actual integration.
Make the AI's decisions visible. For wetter-sport, the assistant chose the weather provider, default location, historical window, and thresholds for identifying weather-sensitive products. Those decisions were documented in the README. You don't need to make every technical decision yourself, but you should be able to see the decisions the assistant made and change them later.
Keep your development setup predictable. Port conflicts came up repeatedly in the prompt histories. Local URLs are part of the app manifest, so changing ports can mean updating and registering the app again. Pick your ports early and keep them stable.
From Conference Demo to Real JTL Cloud App
Getting an app running inside the Cloud ERP is the exciting part. Shipping it to merchants is where the engineering work really begins. Before turning one of these experiments into a production application:
- Trim the scope to what the app reads and writes today.
-
Host it on HTTPS and swap every
localhostURL in the manifest for the real one. - Handle the unhappy paths. An expired token, an empty tenant, a slow API, a customer with no orders. Live demos rarely hit these. Merchants hit them on day one.
-
Keep secrets out of the repo. The template's
.gitignorealready covers.envandappsettings.Local.json. Keep it that way. - Know your data gaps. If a number depends on data the API can't give you yet (returns, in Kundenrisiko's case), say so in the UI.
- Test with a real tenant, not just the demo route, and with more than a handful of orders.
- Prepare for JTL App Store review if the app is going public, or share it through activation codes if it's for one merchant.
Try it Yourself
The fastest way to learn these patterns is to run one of the apps and change something. Clone the academy-day-examples repo, pick the app closest to your idea, and start from its demo route.
When you're ready to build your own:
- Create an app from the template to get the same starting point these apps used.
- Read up on app-token authentication and Panels in the JTL Developer Docs.
- Connect the JTL Developer Docs MCP server to your AI assistant before your first prompt.
Every app in the repo started as a one-sentence idea from someone in the audience. What's yours?

Top comments (0)