diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts
index 62eddd1a89..a69a2debca 100644
--- a/frontend/src/lib/components/copilot/chat/global/core.ts
+++ b/frontend/src/lib/components/copilot/chat/global/core.ts
@@ -53,8 +53,7 @@ import { evalValue } from '$lib/components/flows/utils.svelte'
import { updateRawAppPolicy } from '$lib/components/raw_apps/rawAppPolicy'
import {
FRAMEWORK_TEMPLATES,
- STARTER_RUNNABLE,
- STARTER_RUNNABLE_KEY,
+ STARTER_RUNNABLES,
type FrameworkKey
} from '$lib/components/raw_apps/templates'
import {
@@ -6411,13 +6410,13 @@ async function initApp(
const value: AppDraftValue = {
summary,
files: { ...template },
- runnables: { [STARTER_RUNNABLE_KEY]: { ...STARTER_RUNNABLE } }
+ runnables: structuredClone(STARTER_RUNNABLES)
}
await recomputeAppPolicy(value)
const result = await saveAppDraft(workspace, path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Saved app "${path}" draft (${framework})`,
- message: `Initialized a per-user draft app "${path}" from the ${framework} template with a starter runnable "${STARTER_RUNNABLE_KEY}" (saved server-side, not a deployed workspace item). Use write_app_file / write_app_runnable to evolve it.`
+ message: `Initialized a per-user draft app "${path}" from the ${framework} template (saved server-side, not a deployed workspace item). The template is a demo tour with starter runnables ${Object.keys(STARTER_RUNNABLES).join(', ')}: replace its UI and delete the runnables the app does not need (delete_app_runnable). Use write_app_file / write_app_runnable to evolve it.`
}))
}
diff --git a/frontend/src/lib/components/raw_apps/templates.ts b/frontend/src/lib/components/raw_apps/templates.ts
index ba630f0720..d6df109262 100644
--- a/frontend/src/lib/components/raw_apps/templates.ts
+++ b/frontend/src/lib/components/raw_apps/templates.ts
@@ -1,3 +1,7 @@
+// The starter app is a tour of what a raw app can do: each card calls one of the
+// STARTER_RUNNABLES below through a different `./wmill` export. The three framework
+// variants share `indexCss` and must stay in step with each other and with the runnables.
+
const reactIndex = `
import React from 'react'
@@ -7,65 +11,500 @@ import App from './App'
const root = createRoot(document.getElementById('root')!);
root.render();
`
+
+const STATS_SAMPLE_TEXT =
+ 'Windmill turns scripts into apps. Write the frontend in React, Svelte or Vue, and the backend in TypeScript, Python or any other supported language. Scripts run on workers, and the app calls them like functions.'
+
const appTsx = `import React, { useState } from 'react'
-import { backend } from './wmill'
+import { backend, backendAsync, getJob, streamJob } from './wmill'
import './index.css'
-const App = () => {
- const [value, setValue] = useState(undefined as string | undefined)
- const [loading, setLoading] = useState(false)
+// Windmill injects the viewer (absent for anonymous visitors) and the workspace.
+const ctx = (window as any).ctx ?? {}
+const viewer: { name?: string; username?: string } | undefined = ctx.ctx
+const firstName = (viewer?.name || viewer?.username || '').split(' ')[0]
- async function runA() {
- setLoading(true)
- try {
- setValue(await backend.a({ x: 42 }))
- } catch (e) {
- console.error()
- }
- setLoading(false)
- }
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
- return
+ A React frontend wired to backend runnables. Each card shows one thing an app can do.
+ Edit App.tsx and the runnables to make it yours, or ask the AI to build on it.
+
+ A Svelte frontend wired to backend runnables. Each card shows one thing an app can do.
+ Edit App.svelte and the runnables to make it yours, or ask the AI to build on it.
+
+
- {#if result}
-
Result: {result}
- {/if}
+
+
+ {@render header('1', 'Call a backend runnable', 'Runnables are scripts that run on Windmill workers. Call them like async functions; context fields add who is calling.')}
+
+
+
+ {#if greetError}
+ {greetError}
+ {:else if greeting}
+ {greeting.message}
+ Ran at {new Date(greeting.ran_at).toLocaleTimeString()}
+ {:else}
+ The result shows up here.
+ {/if}
+
+
+
const res = await backend.greet({ name })
+
+
+
+ {@render header('2', 'Use any language', 'Each runnable picks its own language. This one is Python, called the same way.')}
+
+
+
+
+
+ {#if statsError}
+
{statsError}
+ {/if}
+ {#if stats}
+
+
{stats.words}words
+
{stats.sentences}sentences
+
{stats.characters}characters
+
+ {#each stats.top_words as word (word)}
+ {word}
+ {/each}
+
+
+ {/if}
+
+
const stats = await backend.word_stats({ text })
+
+
+
+ {@render header('3', 'Run jobs in the background', 'Start long jobs without blocking the UI, then poll their status. Start several at once.')}
+
+ A Vue frontend wired to backend runnables. Each card shows one thing an app can do.
+ Edit App.vue and the runnables to make it yours, or ask the AI to build on it.
+
+
+
+
+
+
+ 1
+
+
Call a backend runnable
+
Runnables are scripts that run on Windmill workers. Call them like async functions; context fields add who is calling.
+
+
+
+
+
+ {{ greetError }}
+
+ {{ greeting.message }}
+ Ran at {{ new Date(greeting.ran_at).toLocaleTimeString() }}
+
+ The result shows up here.
+
+
+
const res = await backend.greet({ name })
+
+
+
+
+ 2
+
+
Use any language
+
Each runnable picks its own language. This one is Python, called the same way.
+
+
+
+
+
+
+
+
{{ statsError }}
+
+
{{ stats.words }}words
+
{{ stats.sentences }}sentences
+
{{ stats.characters }}characters
+
+ {{ word }}
+
+
+
+
const stats = await backend.word_stats({ text })
+
+
+
+
+ 3
+
+
Run jobs in the background
+
Start long jobs without blocking the UI, then poll their status. Start several at once.