Skip to Content
AppTemplateUserTasks

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:

FeldBeschreibung
flowNodeInstanceIdEindeutige Task-ID
flowNodeNameAnzeigename des Tasks
processModelIdName des BPMN-Prozesses
userTaskConfig.formFieldsArray der Formularfelder
userTaskConfig.formFields[].idFeld-Bezeichner
userTaskConfig.formFields[].labelAnzeige-Label
userTaskConfig.formFields[].defaultValueOptionaler 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

AspektClient-Side (Polling)Server-Side (Form Action)
RenderingClient ComponentServer Component
AktualitätAutomatisches Polling (3s)Aktuell beim Seitenaufruf
NavigationSingle-Page (State-Toggle)Eigene Route pro Task
Formular-SubmitonSubmit + FormDataaction={serverAction}
SeiteneffekteExplizites loadTasks()Automatisch durch Navigation
Geeignet fürInteraktive DashboardsEinfache Task-Formulare

Nächste Schritte