Six Cloud Apps, Six Patterns: What We Learned Building Live at JTL Connect A team at JTL Academy Day in Cologne built six working JTL Cloud Apps live during a 90-minute session, using Claude Code to turn audience-suggested business ideas into apps inside Cloud ERP. The apps — including Bestandsradar, first-order-welcome, Kundenrisiko, Wetter-Sport, hand-build-app, and Profit Pulse — demonstrate patterns ranging from context-aware Panels and .NET backends to external weather API integration and financial data aggregation, with source code and prompt histories published in the jtl-software/academy-day-examples repository. 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: The source code, screenshots, and prompt histories are available in the academy-day-examples https://github.com/jtl-software/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. 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. 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: npx @jtl-software/create-cloud-app@latest . npm run register . 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. configurationUrl is the setup page the JTL Hub opens during installation. menuItems puts an entry in the Cloud ERP's /erp in an iframe. api.scopes is the data the app may read. Two scopes, both read-only, because a reorder dashboard needs items and sales orders and nothing else. serviceAccount lets 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. 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. 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: 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: js 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 . 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. 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 https://open-meteo.com . 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. 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: js 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. 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. 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. 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: localhost URL in the manifest for the real one. .gitignore already covers .env and appsettings.Local.json . Keep it that way. The fastest way to learn these patterns is to run one of the apps and change something. Clone the academy-day-examples repo https://github.com/jtl-software/academy-day-examples , pick the app closest to your idea, and start from its demo route. When you're ready to build your own: Every app in the repo started as a one-sentence idea from someone in the audience. What's yours?