A headless portal = your own front end (Angular) that lists tasks, opens them and starts processes through the BPM REST API, rendering either your own forms or the BPM coaches inside an iframe. A minimal, working Angular service + component:
// bpm.service.ts (Angular 15+) - same origin as BPM (proxy /rest to the BPM server in dev: proxy.conf.json)
@Injectable({ providedIn: 'root' })
export class BpmService {
private csrf?: string;
constructor(private http: HttpClient) {}
async token(): Promise<string> {
if (this.csrf) return this.csrf;
const r: any = await firstValueFrom(this.http.post('/rest/bpm/wle/v1/system/login', { refresh_groups: false, requested_lifetime: 7200 }));
return this.csrf = r.csrf_token; // BAW 20+ / CP4BA; on 8.5.x skip the token
}
async myTasks(): Promise<any[]> {
const r: any = await firstValueFrom(this.http.put('/rest/bpm/wle/v1/search/query?organization=byTask&run=true&filterByCurrentUser=true&size=100&condition=taskStatus%7CReceived', null,
{ headers: { BPMCSRFToken: await this.token() } }));
return r.data.data; // taskId, taskSubject, taskDueDate, instanceId, bpdName
}
async taskData(id: string) { return (await firstValueFrom(this.http.get<any>(`/rest/bpm/wle/v1/task/${{id}}?parts=data`))).data.data.variables; }
async finish(id: string, output: any) {
return firstValueFrom(this.http.put(`/rest/bpm/wle/v1/task/${{id}}?action=finish¶ms=${{encodeURIComponent(JSON.stringify(output))}}&parts=none`, null, { headers: { BPMCSRFToken: await this.token() } }));
}
async start(bpdId: string, snapshotId: string, input: any) {
return firstValueFrom(this.http.post(`/rest/bpm/wle/v1/process?action=start&bpdId=${{bpdId}}&snapshotId=${{snapshotId}}¶ms=${{encodeURIComponent(JSON.stringify(input))}}&parts=header`, null, { headers: { BPMCSRFToken: await this.token() } }));
}
}<!-- task-list.component.html: own list, coach rendered by BPM inside an iframe when the task's form is a coach -->
<table><tr *ngFor="let t of tasks" (click)="open(t)"><td>{{ t.taskSubject }}</td><td>{{ t.taskDueDate | date }}</td><td>{{ t.bpdName }}</td></tr></table>
<iframe *ngIf="current" [src]="('/teamworks/process.lsw?zWorkflowState=1&zTaskId=' + current.taskId) | safeUrl" style="width:100%;height:80vh;border:0"></iframe>
<!-- or render your own Angular form from taskData() and complete with finish() - the "external implementation" route (question 2898) -->Authentication: same origin (reverse proxy the SPA and BPM under one host) so that the BPM session cookie authenticates the calls; on CP4BA use the Zen session or a bearer token from IAM. Notifications: poll the task list every 30-60 s (there is no push API on traditional BAW; BAW 21+ / CP4BA offer the WebSocket notifications that Workplace uses). Everything Process Portal does (claim, reassign, comments, search, start) exists in the REST API, so the headless portal is a UI project, not an integration project.
References