UserTasks (AppSDK)
UserTasks sind BPMN-Aktivitäten, die eine Benutzereingabe erfordern. Die AppSDK Sample-App zeigt zwei Implementierungsmuster: Client-Side mit Polling und Server-Side mit Form Actions.
Server Actions
Beide Muster nutzen dieselben Server Actions für die Engine-Kommunikation:
app/actions.ts
'use server';
import { getEngineClient } from '@5minds/processcube_app_sdk/server';
function engine() {
return getEngineClient();
}
export async function fetchWaitingUserTasks() {
const result = await engine().userTasks.query({
state: 'suspended' as any,
});
return result?.userTasks ?? [];
}
export async function fetchUserTaskById(flowNodeInstanceId: string) {
const tasks = await fetchWaitingUserTasks();
return tasks.find((t: any) => t.flowNodeInstanceId === flowNodeInstanceId) ?? null;
}
export async function completeUserTask(
flowNodeInstanceId: string,
userTaskResult: Record<string, any>
) {
await engine().userTasks.finishUserTask(flowNodeInstanceId, userTaskResult as any);
}
export async function startSampleProcess() {
await engine().processModels.startProcessInstance({
processModelId: 'SampleWithAppSDK_Process',
startEventId: 'StartEvent_1',
initialToken: {},
});
}Task-Datenstruktur
Jeder UserTask enthält die Formulardefinition:
| Feld | Beschreibung |
|---|---|
flowNodeInstanceId | Eindeutige Task-ID |
flowNodeName | Anzeigename des Tasks |
processModelId | Name des BPMN-Prozesses |
userTaskConfig.formFields | Array der Formularfelder |
userTaskConfig.formFields[].id | Feld-Bezeichner |
userTaskConfig.formFields[].label | Anzeige-Label |
userTaskConfig.formFields[].defaultValue | Optionaler Standardwert |
Pattern 1: Client-Side (Polling)
Die Hauptseite (page.tsx) verwaltet den kompletten Zustand client-seitig mit 3-Sekunden-Polling:
Task-Liste mit Polling
app/page.tsx (Auszug)
'use client';
import { useEffect, useState, useCallback } from 'react';
import { fetchWaitingUserTasks } from './actions';
export default function Home() {
const [tasks, setTasks] = useState<any[]>([]);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const loadTasks = useCallback(async () => {
try {
const result = await fetchWaitingUserTasks();
setTasks(result);
} catch {}
}, []);
// Polling alle 3 Sekunden
useEffect(() => {
let active = true;
async function poll() {
while (active) {
await loadTasks();
await new Promise((r) => setTimeout(r, 3000));
}
}
poll();
return () => { active = false; };
}, [loadTasks]);
// ...
}Dynamisches Formular
Das Formular wird zur Laufzeit aus den formFields des Tasks generiert:
app/page.tsx (Auszug)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!selectedTask) return;
const formData = new FormData(e.currentTarget);
const result: Record<string, any> = {};
for (const field of selectedTask.userTaskConfig?.formFields ?? []) {
result[field.id] = formData.get(field.id) ?? '';
}
await completeUserTask(selectedTask.flowNodeInstanceId, result);
handleBack();
await loadTasks();
}
// Rendering
<form onSubmit={handleSubmit}>
{formFields.map((field: any) => (
<div key={field.id} className="form-group">
<label htmlFor={field.id} className="form-label">
{field.label}
</label>
<input
id={field.id}
name={field.id}
type="text"
defaultValue={field.defaultValue ?? ''}
className="form-input"
/>
</div>
))}
<button type="submit" className="btn btn-primary">
Abschliessen
</button>
</form>Pattern 2: Server-Side (Form Action)
Die Route usertask/[id]/ rendert den Task server-seitig und nutzt eine inline Server Action für das Formular:
app/usertask/[id]/page.tsx
import { fetchWaitingUserTasks, completeUserTask } from '../../actions';
export const dynamic = 'force-dynamic';
export default async function UserTaskPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const userTasks = await fetchWaitingUserTasks();
const task = userTasks.find(
(t: any) => t.flowNodeInstanceId === id
);
if (!task) {
return (
<main>
<h1>Task nicht gefunden</h1>
<a href="/tasks">Zurueck zur Uebersicht</a>
</main>
);
}
const formFields = task.userTaskConfig?.formFields ?? [];
async function submitTask(formData: FormData) {
'use server';
const result: Record<string, any> = {};
for (const field of formFields) {
result[field.id] = formData.get(field.id) ?? '';
}
await completeUserTask(id, result);
}
return (
<main>
<h1>{task.flowNodeName}</h1>
<p>{task.processModelId}</p>
<form action={submitTask}>
{formFields.map((field: any) => (
<div key={field.id}>
<label htmlFor={field.id}>{field.label}</label>
<input
id={field.id}
name={field.id}
type="text"
defaultValue={field.defaultValue ?? ''}
/>
</div>
))}
<button type="submit">Abschliessen</button>
</form>
</main>
);
}export const dynamic = 'force-dynamic' stellt sicher, dass die Seite bei jedem Aufruf serverseitig gerendert wird und immer aktuelle Task-Daten zeigt.
Vergleich der Patterns
| Aspekt | Client-Side (Polling) | Server-Side (Form Action) |
|---|---|---|
| Rendering | Client Component | Server Component |
| Aktualität | Automatisches Polling (3s) | Aktuell beim Seitenaufruf |
| Navigation | Single-Page (State-Toggle) | Eigene Route pro Task |
| Formular-Submit | onSubmit + FormData | action={serverAction} |
| Seiteneffekte | Explizites loadTasks() | Automatisch durch Navigation |
| Geeignet für | Interaktive Dashboards | Einfache Task-Formulare |
Nächste Schritte
- External Tasks — Serverseitige Task-Worker
- Beispielprozess — Kompletter Prozess-Durchlauf
- App-SDK Dokumentation — API-Referenz und Components