frontend_nyla/src/api/workflowApi.ts
2026-05-01 00:00:06 +02:00

1088 lines
31 KiB
TypeScript

/**
* Workflow API (GraphicalEditor)
* Node types and graph execution for n8n-style flows.
*/
import type { ApiRequestOptions } from '../hooks/useApi';
const LOG = '[Workflow]';
// ============================================================================
// TYPES
// ============================================================================
export interface NodeTypeParameter {
name: string;
type: string;
required?: boolean;
description?: string;
default?: unknown;
frontendType?: string;
frontendOptions?: Record<string, unknown>;
options?: unknown[];
validation?: Record<string, unknown>;
}
export interface PortField {
name: string;
type: string;
/** Plain string or per-language map from the API catalog. */
description: string | Record<string, string>;
required: boolean;
enumValues?: string[] | null;
}
export interface PortSchema {
name: string;
fields: PortField[];
}
export interface InputPortDef {
accepts: string[];
}
/** Graph-defined output schema (e.g. form fields from node parameters). */
export interface GraphDefinedSchemaRef {
kind: 'fromGraph';
parameter: string;
}
export interface OutputPortDef {
schema: string | GraphDefinedSchemaRef;
dynamic?: boolean;
deriveFrom?: string;
}
export interface NodeType {
id: string;
category: string;
label: string;
description: string;
parameters: NodeTypeParameter[];
inputs: number;
outputs: number;
outputLabels?: string[];
executor: string;
inputPorts?: Record<number, InputPortDef>;
outputPorts?: Record<number, OutputPortDef>;
meta?: {
icon?: string;
color?: string;
/** True if this node performs an LLM / AI call (credits). */
usesAi?: boolean;
method?: string;
action?: string;
};
}
export interface NodeTypeCategory {
id: string;
label: Record<string, string> | string;
}
export interface SystemVariable {
type: string;
description: string;
}
export interface NodeTypesResponse {
nodeTypes: NodeType[];
categories: NodeTypeCategory[];
portTypeCatalog?: Record<string, PortSchema>;
systemVariables?: Record<string, SystemVariable>;
}
export interface Automation2GraphNode {
id: string;
type: string;
parameters?: Record<string, unknown>;
inputPorts?: Array<{ name: string; schema: string; accepts?: string[] }>;
outputPorts?: Array<{ name: string; schema: string | GraphDefinedSchemaRef }>;
}
export interface Automation2Connection {
source: string;
target: string;
sourceOutput?: number;
targetInput?: number;
}
export interface Automation2Graph {
nodes: Automation2GraphNode[];
connections: Automation2Connection[];
}
export interface ExecuteGraphResponse {
success: boolean;
nodeOutputs?: Record<string, unknown>;
error?: string;
/** Soft, non-blocking message displayed alongside a successful response.
* Used e.g. by the Save flow to surface "Gespeichert mit X Pflicht-Fehlern"
* without flipping `success` to `false`. */
warning?: string;
stopped?: boolean;
failedNode?: string;
paused?: boolean;
taskId?: string;
runId?: string;
nodeId?: string;
}
/** Entry point / start configured outside the canvas (manual, form, schedule, …) */
export interface WorkflowEntryPoint {
id: string;
kind: string;
category: 'on_demand' | 'always_on';
enabled: boolean;
title: Record<string, string> | string;
description?: Record<string, string>;
config: Record<string, unknown>;
}
export interface Automation2Workflow {
id: string;
label: string;
graph: Automation2Graph;
active?: boolean;
/** Target feature instance for execution data scope (NULL for templates) */
targetFeatureInstanceId?: string | null;
/** Entry points (Starts) — how this workflow may be invoked */
invocations?: WorkflowEntryPoint[];
/** Enriched: run count */
runCount?: number;
/** Enriched: has active (running/paused) run */
isRunning?: boolean;
/** Enriched: status of active run */
runStatus?: string;
/** Enriched: nodeId where workflow is stuck (paused) */
stuckAtNodeId?: string;
/** Enriched: human-readable label for stuck node */
stuckAtNodeLabel?: string;
/** From PowerOnModel base — record creation timestamp (seconds) */
sysCreatedAt?: number;
/** Enriched: last run started timestamp (seconds) */
lastStartedAt?: number;
}
// ============================================================================
// AUTO-PREFIX TYPES (Greenfield)
// ============================================================================
export type AutoWorkflowStatus = 'draft' | 'published' | 'archived';
export type AutoRunStatus = 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
export type AutoStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
export type AutoTaskStatus = 'pending' | 'completed' | 'cancelled' | 'expired';
export type AutoTemplateScope = 'user' | 'instance' | 'mandate' | 'system';
export interface AutoVersion {
id: string;
workflowId: string;
versionNumber: number;
status: AutoWorkflowStatus;
graph: Automation2Graph;
invocations?: WorkflowEntryPoint[];
publishedAt?: number;
publishedBy?: string;
}
export interface AutoRun {
id: string;
workflowId: string;
versionId?: string;
status: AutoRunStatus;
trigger?: Record<string, unknown>;
startedAt?: number;
completedAt?: number;
nodeOutputs?: Record<string, unknown>;
currentNodeId?: string;
resumeContext?: Record<string, unknown>;
error?: string;
costTokens?: number;
costCredits?: number;
}
export interface AutoWorkflow {
id: string;
mandateId: string;
featureInstanceId: string;
label: string;
description?: string;
tags?: string[];
isTemplate: boolean;
templateSourceId?: string;
templateScope?: AutoTemplateScope;
sharedReadOnly?: boolean;
currentVersionId?: string;
active: boolean;
eventId?: string;
notifyOnFailure?: boolean;
graph: Automation2Graph;
invocations?: WorkflowEntryPoint[];
sysCreatedBy?: string;
sysCreatedAt?: number;
sysModifiedBy?: string;
sysModifiedAt?: number;
}
export interface AutoTask {
id: string;
runId: string;
workflowId: string;
nodeId: string;
nodeType: string;
config: Record<string, unknown>;
assigneeId?: string;
status: AutoTaskStatus;
result?: Record<string, unknown>;
expiresAt?: number;
sysCreatedAt?: number;
}
export interface AutoStepLog {
id: string;
runId: string;
nodeId: string;
nodeType: string;
status: AutoStepStatus;
inputSnapshot?: Record<string, unknown>;
output?: Record<string, unknown>;
error?: string;
startedAt?: number;
completedAt?: number;
durationMs?: number;
tokensUsed?: number;
retryCount?: number;
}
// ============================================================================
// API FUNCTIONS
// ============================================================================
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
/**
* Fetch node types for the flow builder (backend-driven).
* GET /api/workflows/{instanceId}/node-types?language=de
*/
export async function fetchNodeTypes(
request: ApiRequestFunction,
instanceId: string,
language = 'de'
): Promise<NodeTypesResponse> {
console.log(`${LOG} fetchNodeTypes: instanceId=${instanceId} language=${language}`);
const data = await request({
url: `/api/workflows/${instanceId}/node-types`,
method: 'get',
params: { language },
});
const nodeTypes = data?.nodeTypes ?? [];
const categories = data?.categories ?? [];
const portTypeCatalog = data?.portTypeCatalog ?? undefined;
const systemVariables = data?.systemVariables ?? undefined;
console.log(
`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
`${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
`${systemVariables ? Object.keys(systemVariables).length : 0} sysVars`
);
return { nodeTypes, categories, portTypeCatalog, systemVariables };
}
export interface UpstreamPathEntry {
producerNodeId: string;
producerLabel?: string;
path: (string | number)[];
type: string;
label: string;
scopeOrigin: 'data' | 'loop' | 'system';
}
/**
* POST /api/workflows/{instanceId}/upstream-paths — pickable upstream paths for DataPicker / AI.
*/
export async function postUpstreamPaths(
request: ApiRequestFunction,
instanceId: string,
graph: Automation2Graph,
nodeId: string
): Promise<{ paths: UpstreamPathEntry[] }> {
const data = await request({
url: `/api/workflows/${instanceId}/upstream-paths`,
method: 'post',
data: { graph, nodeId },
});
return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
}
/** GET saved workflow graph variant of upstream-paths (requires workflowId). */
export async function getUpstreamPathsSaved(
request: ApiRequestFunction,
instanceId: string,
workflowId: string,
nodeId: string
): Promise<{ paths: UpstreamPathEntry[] }> {
const data = await request({
url: `/api/workflows/${instanceId}/upstream-paths/${encodeURIComponent(nodeId)}`,
method: 'get',
params: { workflowId },
});
return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
}
/**
* Execute an automation2 graph.
* POST /api/workflows/{instanceId}/execute
*/
export interface ExecuteGraphOptions {
/** Use a configured start on the saved workflow */
entryPointId?: string;
/** Full run envelope (overrides entry point mapping) */
runEnvelope?: Record<string, unknown>;
/** Merged into envelope.payload */
payload?: Record<string, unknown>;
}
export async function executeGraph(
request: ApiRequestFunction,
instanceId: string,
graph: Automation2Graph,
workflowId?: string,
options?: ExecuteGraphOptions
): Promise<ExecuteGraphResponse> {
console.log(
`${LOG} executeGraph request: instanceId=${instanceId} workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`,
{ nodes: graph.nodes, connections: graph.connections, options }
);
const start = performance.now();
try {
const data: Record<string, unknown> = { graph, workflowId };
if (options?.entryPointId) data.entryPointId = options.entryPointId;
if (options?.runEnvelope) data.runEnvelope = options.runEnvelope;
if (options?.payload && Object.keys(options.payload).length > 0) data.payload = options.payload;
const result = await request({
url: `/api/workflows/${instanceId}/execute`,
method: 'post',
data,
});
const ms = Math.round(performance.now() - start);
console.log(
`${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`,
result
);
return result;
} catch (err) {
const ms = Math.round(performance.now() - start);
console.error(
`${LOG} executeGraph FAILED (${ms}ms): instanceId=${instanceId}`,
err
);
throw err;
}
}
// -------------------------------------------------------------------------
// Workflows CRUD
// -------------------------------------------------------------------------
export async function fetchWorkflows(
request: ApiRequestFunction,
instanceId: string,
params?: { active?: boolean; pagination?: any }
): Promise<Automation2Workflow[] | { items: Automation2Workflow[]; pagination: any }> {
const queryParams: Record<string, any> = {};
if (params?.active !== undefined) queryParams.active = params.active;
if (params?.pagination) queryParams.pagination = JSON.stringify(params.pagination);
const data = await request({
url: `/api/workflows/${instanceId}/workflows`,
method: 'get',
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
});
if (data?.items && data?.pagination) return data;
return data?.workflows ?? [];
}
export async function fetchWorkflow(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<Automation2Workflow> {
return await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}`,
method: 'get',
});
}
export async function createWorkflow(
request: ApiRequestFunction,
instanceId: string,
body: {
label: string;
graph: Automation2Graph;
invocations?: WorkflowEntryPoint[];
targetFeatureInstanceId?: string | null;
}
): Promise<Automation2Workflow> {
return await request({
url: `/api/workflows/${instanceId}/workflows`,
method: 'post',
data: body,
});
}
export async function updateWorkflow(
request: ApiRequestFunction,
instanceId: string,
workflowId: string,
body: {
label?: string;
graph?: Automation2Graph;
invocations?: WorkflowEntryPoint[];
active?: boolean;
notifyOnFailure?: boolean;
targetFeatureInstanceId?: string | null;
}
): Promise<Automation2Workflow> {
return await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}`,
method: 'put',
data: body,
});
}
export async function deleteWorkflow(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<void> {
await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}`,
method: 'delete',
});
}
// -------------------------------------------------------------------------
// Workflow file IO (envelopeVersioned, .workflow.json)
// -------------------------------------------------------------------------
/** envelopeVersioned schema 1.0 — keys mirror the gateway constants. */
export const WORKFLOW_FILE_SCHEMA_VERSION = '1.0';
export const WORKFLOW_FILE_KIND = 'poweron.workflow';
export const WORKFLOW_FILE_EXTENSION = '.workflow.json';
export interface WorkflowFileEnvelope {
$schemaVersion: string;
$kind: string;
$exportedAt?: string;
$gatewayVersion?: string;
label: string;
description?: string;
tags?: string[];
templateScope?: AutoTemplateScope;
sharedReadOnly?: boolean;
notifyOnFailure?: boolean;
graph: Automation2Graph;
invocations?: WorkflowEntryPoint[];
}
export interface ImportWorkflowResponse {
workflow: AutoWorkflow;
warnings: string[];
created: boolean;
}
export interface ImportWorkflowOptions {
/** Inline envelope payload (preferred for round-trip in the editor). */
envelope?: WorkflowFileEnvelope;
/** UDB FileItem.id of a previously uploaded ``.workflow.json``. */
fileId?: string;
/** When set, the existing workflow is replaced instead of a new one being created. */
existingWorkflowId?: string;
}
/** POST /api/workflows/{instanceId}/workflows/import */
export async function importWorkflowFromFile(
request: ApiRequestFunction,
instanceId: string,
options: ImportWorkflowOptions,
): Promise<ImportWorkflowResponse> {
if (!options.envelope && !options.fileId) {
throw new Error('importWorkflowFromFile: either envelope or fileId is required');
}
return await request({
url: `/api/workflows/${instanceId}/workflows/import`,
method: 'post',
data: options,
});
}
export interface ExportWorkflowResult {
fileName: string;
envelope: WorkflowFileEnvelope;
}
/**
* GET /api/workflows/{instanceId}/workflows/{workflowId}/export
*
* The backend returns ``{ fileName, envelope }`` when ``download=false`` and a
* raw JSON download (``Content-Disposition: attachment``) when ``download=true``.
* For programmatic use (e.g. re-uploading to UDB) keep download=false.
*/
export async function exportWorkflowToFile(
request: ApiRequestFunction,
instanceId: string,
workflowId: string,
download = false,
): Promise<ExportWorkflowResult> {
return await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}/export`,
method: 'get',
params: { download },
});
}
/** Quick content-sniffing — used by the UDB FilesTab to flag workflow files. */
export function isWorkflowFileContent(payload: unknown): boolean {
if (!payload || typeof payload !== 'object') return false;
const p = payload as Record<string, unknown>;
return (
typeof p.$schemaVersion === 'string' &&
p.$kind === WORKFLOW_FILE_KIND &&
!!p.graph &&
typeof p.graph === 'object'
);
}
/** Suggest a safe filename from the workflow label (mirrors gateway buildFileName). */
export function workflowFileNameFor(label: string): string {
const slug = (label || 'workflow')
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80) || 'workflow';
return `${slug}${WORKFLOW_FILE_EXTENSION}`;
}
/** Delete by workflow ID only (Automations dashboard / orphan rows without featureInstanceId). */
export async function deleteSystemWorkflow(
request: ApiRequestFunction,
workflowId: string,
): Promise<void> {
await request({
url: `/api/system/workflow-runs/workflows/${workflowId}`,
method: 'delete',
});
}
export interface Automation2Run {
id: string;
workflowId: string;
status: string;
nodeOutputs?: Record<string, unknown>;
currentNodeId?: string;
}
export async function fetchWorkflowRuns(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<Automation2Run[]> {
const data = await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}/runs`,
method: 'get',
});
return data?.runs ?? [];
}
export interface CompletedRun extends Automation2Run {
workflowLabel?: string;
sysModifiedAt?: number;
sysCreatedAt?: number;
}
export async function fetchCompletedRuns(
request: ApiRequestFunction,
instanceId: string,
limit = 20
): Promise<CompletedRun[]> {
const data = await request({
url: `/api/workflows/${instanceId}/runs/completed`,
method: 'get',
params: { limit },
});
return data?.runs ?? [];
}
// -------------------------------------------------------------------------
// Tasks
// -------------------------------------------------------------------------
export interface Automation2Task {
id: string;
runId: string;
workflowId: string;
nodeId: string;
nodeType: string;
config: Record<string, unknown>;
status: string;
result?: Record<string, unknown>;
/** Workflow label (enriched by API) */
workflowLabel?: string;
/** Unix timestamp ms (from sysCreatedAt) */
createdAt?: number;
/** Optional due date - configurable in future */
dueAt?: number;
}
export async function fetchTasks(
request: ApiRequestFunction,
instanceId: string,
params?: { workflowId?: string; status?: string }
): Promise<Automation2Task[]> {
const data = await request({
url: `/api/workflows/${instanceId}/tasks`,
method: 'get',
params,
});
return data?.tasks ?? [];
}
export async function completeTask(
request: ApiRequestFunction,
instanceId: string,
taskId: string,
result: Record<string, unknown>
): Promise<ExecuteGraphResponse> {
return await request({
url: `/api/workflows/${instanceId}/tasks/${taskId}/complete`,
method: 'post',
data: { result },
});
}
// -------------------------------------------------------------------------
// Versions (AutoVersion Lifecycle)
// -------------------------------------------------------------------------
export async function fetchVersions(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<AutoVersion[]> {
const data = await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}/versions`,
method: 'get',
});
return data?.versions ?? [];
}
export async function createDraftVersion(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<AutoVersion> {
return await request({
url: `/api/workflows/${instanceId}/workflows/${workflowId}/versions/draft`,
method: 'post',
});
}
export async function publishVersion(
request: ApiRequestFunction,
instanceId: string,
versionId: string
): Promise<AutoVersion> {
return await request({
url: `/api/workflows/${instanceId}/versions/${versionId}/publish`,
method: 'post',
});
}
export async function unpublishVersion(
request: ApiRequestFunction,
instanceId: string,
versionId: string
): Promise<AutoVersion> {
return await request({
url: `/api/workflows/${instanceId}/versions/${versionId}/unpublish`,
method: 'post',
});
}
export async function archiveVersion(
request: ApiRequestFunction,
instanceId: string,
versionId: string
): Promise<AutoVersion> {
return await request({
url: `/api/workflows/${instanceId}/versions/${versionId}/archive`,
method: 'post',
});
}
// -------------------------------------------------------------------------
// Templates
// -------------------------------------------------------------------------
export interface AutoWorkflowTemplate extends Automation2Workflow {
isTemplate: boolean;
templateScope?: AutoTemplateScope;
templateSourceId?: string;
sharedReadOnly?: boolean;
}
export async function fetchTemplates(
request: ApiRequestFunction,
instanceId: string,
scope?: AutoTemplateScope,
pagination?: any
): Promise<AutoWorkflowTemplate[] | { items: AutoWorkflowTemplate[]; pagination: any }> {
const queryParams: Record<string, any> = {};
if (scope) queryParams.scope = scope;
if (pagination) queryParams.pagination = JSON.stringify(pagination);
const data = await request({
url: `/api/workflows/${instanceId}/templates`,
method: 'get',
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
});
if (data?.items && data?.pagination) return data;
return data?.templates ?? [];
}
export async function createTemplateFromWorkflow(
request: ApiRequestFunction,
instanceId: string,
workflowId: string,
scope: AutoTemplateScope = 'user'
): Promise<AutoWorkflowTemplate> {
return await request({
url: `/api/workflows/${instanceId}/templates/from-workflow`,
method: 'post',
data: { workflowId, scope },
});
}
export async function copyTemplate(
request: ApiRequestFunction,
instanceId: string,
templateId: string
): Promise<Automation2Workflow> {
return await request({
url: `/api/workflows/${instanceId}/templates/${templateId}/copy`,
method: 'post',
});
}
export async function shareTemplate(
request: ApiRequestFunction,
instanceId: string,
templateId: string,
scope: AutoTemplateScope
): Promise<AutoWorkflowTemplate> {
return await request({
url: `/api/workflows/${instanceId}/templates/${templateId}/share`,
method: 'post',
data: { scope },
});
}
// -------------------------------------------------------------------------
// Connections and Browse (for Email/SharePoint node config)
// -------------------------------------------------------------------------
export interface UserConnection {
id: string;
authority: string;
externalUsername?: string;
externalEmail?: string;
status: string;
}
export async function fetchConnections(
request: ApiRequestFunction,
instanceId: string
): Promise<UserConnection[]> {
const data = await request({
url: `/api/workflows/${instanceId}/connections`,
method: 'get',
});
return data?.connections ?? [];
}
export interface ConnectionService {
service: string;
label: string;
icon: string;
}
export async function fetchConnectionServices(
request: ApiRequestFunction,
instanceId: string,
connectionId: string
): Promise<ConnectionService[]> {
const data = await request({
url: `/api/workflows/${instanceId}/connections/${connectionId}/services`,
method: 'get',
});
return data?.services ?? [];
}
export interface BrowseEntry {
name: string;
path: string;
isFolder: boolean;
size?: number;
mimeType?: string;
metadata?: Record<string, unknown>;
}
export async function fetchBrowse(
request: ApiRequestFunction,
instanceId: string,
connectionId: string,
service: string,
path = '/'
): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
const data = await request({
url: `/api/workflows/${instanceId}/connections/${connectionId}/browse`,
method: 'get',
params: { service, path },
});
return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service };
}
/** ClickUp GET /task/{taskId} — list.id for resolving list-scoped fields when only task id is known. */
export async function fetchClickupTask(
request: ApiRequestFunction,
connectionId: string,
taskId: string
): Promise<Record<string, unknown>> {
const data = await request({
url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`,
method: 'get',
});
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
}
/** ClickUp list metadata (statuses, etc.) — GET /api/clickup/{connectionId}/lists/{listId}. */
export async function fetchClickupList(
request: ApiRequestFunction,
connectionId: string,
listId: string
): Promise<Record<string, unknown>> {
const data = await request({
url: `/api/clickup/${connectionId}/lists/${listId}`,
method: 'get',
});
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
}
/** ClickUp workspace/team (members for assignees) — GET /api/clickup/{connectionId}/teams/{teamId}. */
export async function fetchClickupTeam(
request: ApiRequestFunction,
connectionId: string,
teamId: string
): Promise<Record<string, unknown>> {
const data = await request({
url: `/api/clickup/${connectionId}/teams/${teamId}`,
method: 'get',
});
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
}
/** ClickUp list custom fields (GET /api/clickup/{connectionId}/lists/{listId}/fields). */
export async function fetchClickupListFields(
request: ApiRequestFunction,
connectionId: string,
listId: string
): Promise<{ fields?: unknown[] } & Record<string, unknown>> {
const data = await request({
url: `/api/clickup/${connectionId}/lists/${listId}/fields`,
method: 'get',
});
return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record<string, unknown>;
}
/** ClickUp GET /list/{id}/task page (tasks in a list for relationship dropdowns). */
export interface ClickupListTaskItem {
id?: string;
name?: string;
}
export async function fetchClickupListTasks(
request: ApiRequestFunction,
connectionId: string,
listId: string,
options?: { page?: number; includeClosed?: boolean }
): Promise<
{ tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record<string, unknown>
> {
const data = await request({
url: `/api/clickup/${connectionId}/lists/${listId}/tasks`,
method: 'get',
params: {
page: options?.page ?? 0,
include_closed: options?.includeClosed ?? false,
},
});
return (data && typeof data === 'object' ? data : {}) as {
tasks?: ClickupListTaskItem[];
last_page?: boolean;
} & Record<string, unknown>;
}
// -------------------------------------------------------------------------
// Monitoring / Metrics
// -------------------------------------------------------------------------
export interface WorkflowMetrics {
workflowCount: number;
activeWorkflows: number;
totalRuns: number;
runsByStatus: Record<string, number>;
totalTasks: number;
tasksByStatus: Record<string, number>;
totalTokens: number;
totalCredits: number;
}
export async function fetchMetrics(
request: ApiRequestFunction,
instanceId: string
): Promise<WorkflowMetrics> {
return await request({
url: `/api/workflows/${instanceId}/metrics`,
method: 'get',
});
}
/** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe“. */
export async function loadClickupListTasksForDropdown(
request: ApiRequestFunction,
connectionId: string,
listId: string
): Promise<Array<{ id: string; name: string }>> {
const acc: Array<{ id: string; name: string }> = [];
const seen = new Set<string>();
const maxPages = 12;
const pageSizeHint = 100;
for (let page = 0; page < maxPages; page++) {
const data = await fetchClickupListTasks(request, connectionId, listId, {
page,
includeClosed: false,
});
if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) {
const err = (data as { error?: unknown }).error;
const body = (data as { body?: string }).body;
throw new Error(
typeof err === 'string' ? err + (body ? `: ${body.slice(0, 200)}` : '') : 'ClickUp API error'
);
}
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
for (const t of tasks) {
const id = t?.id != null ? String(t.id) : '';
if (!id || seen.has(id)) continue;
seen.add(id);
acc.push({ id, name: String(t.name ?? id) });
}
const rawLast = (data as Record<string, unknown>).last_page;
const last =
rawLast === true ||
rawLast === 'true' ||
tasks.length === 0 ||
tasks.length < pageSizeHint;
if (last) break;
}
acc.sort((a, b) => a.name.localeCompare(b.name, 'de'));
return acc;
}
// ============================================================================
// AUTOMATION WORKSPACE API (user-facing run workspace)
// ============================================================================
export interface WorkspaceRun {
id: string;
workflowId: string;
workflowLabel?: string;
status: string;
startedAt?: number;
completedAt?: number;
ownerId?: string;
mandateId?: string;
mandateLabel?: string;
targetFeatureInstanceId?: string;
targetInstanceLabel?: string;
costTokens?: number;
costCredits?: number;
error?: string;
}
export interface WorkspaceRunDetail {
run: WorkspaceRun & { nodeOutputs?: Record<string, unknown> };
workflow: {
id: string;
label: string;
targetFeatureInstanceId?: string;
featureInstanceId?: string;
tags?: string[];
} | null;
steps: Array<{
id: string;
runId: string;
nodeId: string;
nodeType: string;
status: string;
inputSnapshot?: Record<string, unknown>;
output?: Record<string, unknown>;
inputFiles?: Array<{ id: string; fileName?: string }>;
outputFiles?: Array<{ id: string; fileName?: string }>;
error?: string;
startedAt?: number;
completedAt?: number;
durationMs?: number;
tokensUsed?: number;
retryCount?: number;
}>;
files: Array<{
id: string;
fileName?: string;
contentType?: string;
sizeBytes?: number;
}>;
unassignedFiles?: Array<{
id: string;
fileName?: string;
}>;
}
export async function fetchWorkspaceRuns(
request: ApiRequestFunction,
params: {
scope?: 'mine' | 'mandate';
status?: string;
targetInstanceId?: string;
workflowId?: string;
limit?: number;
offset?: number;
} = {},
): Promise<{ runs: WorkspaceRun[]; total: number }> {
const query = new URLSearchParams();
if (params.scope) query.set('scope', params.scope);
if (params.status) query.set('status', params.status);
if (params.targetInstanceId) query.set('targetInstanceId', params.targetInstanceId);
if (params.workflowId) query.set('workflowId', params.workflowId);
if (params.limit) query.set('limit', String(params.limit));
if (params.offset) query.set('offset', String(params.offset));
const qs = query.toString();
const url = `/api/automations/runs${qs ? `?${qs}` : ''}`;
const resp = await request({ url, method: 'get' });
return resp as { runs: WorkspaceRun[]; total: number };
}
export async function fetchWorkspaceRunDetail(
request: ApiRequestFunction,
runId: string,
): Promise<WorkspaceRunDetail> {
const resp = await request({ url: `/api/automations/runs/${runId}/detail`, method: 'get' });
return resp as WorkspaceRunDetail;
}