Pembuatan Addon pada AlurKerja
Integrasi ke MockAPI Test AddOn
1. Arsitektur dan Konsep Dasar
- Index.json berfungsi Manifest utama addon, untuk mendaftarkan semua komponents
- Custom Views untuk form UI dengan konfigurasi addon dan input action
- Scripts berfungsi sebagai Logika bisnis untuk memanggil OpenAI ChatGPT API
- Actions endpoint yang bisa di gunakan di BPMN workflow
2. Structure Folder Addon
addon-integrasi-api-chatgpt/
├── index.json ← Manifest utama addon
├── README.md ← Dokumentasi addon
├── HOW-TO.md ← Panduan pembuatan
├── scripts/ ← Action scripts (Python)
│ ├── README.MD ← Panduan penulisan script
│ ├── requirements.txt ← Dependensi Python
│ ├── test_integration.py ← Script test integrasi
│ └── send_completion_request/
│ ├── send_completion_request.py ← Script utama ChatGPT
│ └── request.example.json ← Contoh request payload
└── views/ ← Micro Frontend (React)
├── package.json
├── webpack.config.js ← Konfigurasi MFE + Module Federation
├── tailwind.config.js
├── tsconfig.json
├── postcss.config.js
├── public/
│ └── index.html
├── dist/ ← Hasil build (jangan hapus)
│ ├── bundle.js
│ └── remoteEntry.js ← Diload oleh Alurkerja
└── src/
├── index.js ← Entry point
├── bootstrap.js ← Export semua komponen
├── App.tsx
├── ChatGPTConfigForm.tsx ← Form konfigurasi API
├── AskChatGPTView.tsx ← Form input action
└── type/
└── AlurkerjaType.ts ← TypeScript types3. Structure File index.json
name → Nama addon yang akan di tampilkan di UI alurkerja
addon_key → identifier unik
view_type → di gunakan untuk MFE
view_path → Path untuk build webpack
components_scope → Harus sama dengan name di webpack.confi.js → ModuleFederationPlugin
scripts → Deklarasi script Phyton yang bisa di panggil
actions → Action yang akan muncul sebagai pilihan di BPMN service Task4. Install AlurKerja Click
npm install -g alurkerja-cli5. Clone Project
https://gitlab.javan.co.id/alurkerja/on-premises/addons/addon-integrasi-api-chatgpt.git
cd views
npm install 6. Set Up MFE
- Paste file views/webpack.config.js :
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
mode: 'development',
entry: path.resolve(__dirname, './src/index.js'),
devServer: {
port: 3001, // Port dev server lokal
historyApiFallback: true,
},
output: {
publicPath: 'auto' // Penting untuk Module Federation
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
module: {
rules: [
{ test: /\.(ts|tsx|js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] },
{ test: /\.(png|jpe?g|gif|svg)$/i, type: 'asset/resource' }
]
},
plugins: [
new ModuleFederationPlugin({
name: 'chatgpt_view', // ← HARUS SAMA dengan component_scope di index.json
filename: 'remoteEntry.js', // ← File ini yang di-load Alurkerja
exposes: {
// Key: nama modul yang di-expose
// Value: path file komponen
'./chat_gpt_config': './src/ChatGPTConfigForm.tsx',
'./ask_chatgpt_view': './src/AskChatGPTView.tsx',
},
shared: {
react: { singleton: true }, // Hanya satu instance React
'react-dom': { singleton: true },
'react-hook-form': { singleton: true },
leaflet: { singleton: true },
}
}),
new HtmlWebpackPlugin({ template: './public/index.html' })
]
};- Buat TypeScript at views/src/type/AlurkerjaType.ts
import { UseFormReturn } from 'react-hook-form';
export interface AlurkerjaMfeProps {
form: UseFormReturn<any>; // React Hook Form instance
alurkerjaParams?: {
[key: string]: any; // Parameter dari platform
};
}7. Buat Konfigurasi View
- Buat komponents views/src/ChatGPTConfigForm.tsx dengan full code di bawah ini:
import React from 'react';
import { AlurkerjaMfeProps } from './type/AlurkerjaType';
const ChatGPTConfigForm: React.FC<AlurkerjaMfeProps> = ({ form, alurkerjaParams }) => {
const { register } = form;
return (
<div className="space-y-4 p-4">
<h3 className="text-lg font-semibold">Konfigurasi ChatGPT</h3>
{/* API Key */}
<div>
<label className="block text-sm font-medium mb-1">
API Key <span className="text-red-500">*</span>
</label>
<input
{...register('api_key')}
type="password"
placeholder="sk-xxxxxxxxxxxx"
className="w-full border rounded px-3 py-2"
/>
<p className="text-xs text-gray-500 mt-1">
Dapatkan API Key dari <a href="https://platform.openai.com/api-keys" target="_blank" rel="noreferrer">platform.openai.com</a>
</p>
</div>
{/* Model Selection */}
<div>
<label className="block text-sm font-medium mb-1">Model</label>
<select {...register('model')} className="w-full border rounded px-3 py-2">
<option value="gpt-3.5-turbo">GPT-3.5 Turbo (Hemat)</option>
<option value="gpt-4">GPT-4 (Lebih Pintar)</option>
<option value="gpt-4-turbo">GPT-4 Turbo</option>
<option value="gpt-4o">GPT-4o (Terbaru)</option>
</select>
</div>
{/* System Prompt */}
<div>
<label className="block text-sm font-medium mb-1">System Prompt</label>
<textarea
{...register('system_prompt')}
rows={3}
placeholder="You are a helpful assistant..."
className="w-full border rounded px-3 py-2"
defaultValue="You are a helpful assistant."
/>
</div>
</div>
);
};
export default ChatGPTConfigForm;8. Membuat Action View
- views/src/AskChatGPTView.tsx dengan full code di bawah ini:
import React from 'react';
import { AlurkerjaMfeProps } from './type/AlurkerjaType';
const AskChatGPTView: React.FC<AlurkerjaMfeProps> = ({ form, alurkerjaParams }) => {
const { register } = form;
return (
<div className="space-y-4 p-4">
<h3 className="text-lg font-semibold">Ask ChatGPT</h3>
{/* Prompt Input */}
<div>
<label className="block text-sm font-medium mb-1">
Prompt / Pertanyaan <span className="text-red-500">*</span>
</label>
<textarea
{...register('prompt')}
rows={4}
placeholder="Contoh: Rangkum dokumen berikut: ${variables.document}"
className="w-full border rounded px-3 py-2"
/>
<p className="text-xs text-gray-500 mt-1">
Gunakan <code>{'${variables.namaVariable}'}</code> untuk menyisipkan data dari workflow
</p>
</div>
{/* Temperature */}
<div>
<label className="block text-sm font-medium mb-1">
Temperature (0 – 2)
</label>
<input
{...register('temperature')}
type="number"
step="0.1"
min="0"
max="2"
defaultValue="0.7"
className="w-full border rounded px-3 py-2"
/>
<p className="text-xs text-gray-500 mt-1">
0 = deterministic (konsisten), 2 = kreatif/acak
</p>
</div>
{/* Max Tokens */}
<div>
<label className="block text-sm font-medium mb-1">Max Tokens</label>
<input
{...register('max_tokens')}
type="number"
defaultValue="1000"
min="1"
max="4096"
className="w-full border rounded px-3 py-2"
/>
<p className="text-xs text-gray-500 mt-1">
Batas panjang response AI. 1000 token ≈ 750 kata
</p>
</div>
</div>
);
};
export default AskChatGPTView;9. Buat Python Script sebagai Logika utama
- Buat file di scripts/send_completion_request/send_completion_request.py dengan full code di bawah ini:
import json
import argparse
import requests
def run(ctx):
"""
Mengirim request ke OpenAI ChatGPT API.
Execution Context (ctx):
- ctx["parameters"] : Input dari user di BPMN (prompt, temperature, max_tokens)
- ctx["configuration"]: Konfigurasi addon (api_key, model, system_prompt)
- ctx["variables"] : Data runtime dari workflow
- ctx["runkey"] : Unique identifier untuk logging/tracing
"""
# ── 1. Ambil konfigurasi addon ──────────────────────────────────────────
config = ctx.get("configuration", {})
api_key = config.get("api_key")
model = config.get("model", "gpt-3.5-turbo")
system_prompt = config.get("system_prompt", "You are a helpful assistant.")
# ── 2. Ambil parameter dari user (input di BPMN) ─────────────────────────
params = ctx.get("parameters", {})
prompt = params.get("prompt", "")
temperature = float(params.get("temperature", 0.7))
max_tokens = int(params.get("max_tokens", 1000))
# ── 3. Validasi input ────────────────────────────────────────────────────
if not api_key:
raise ValueError("API Key tidak ditemukan dalam konfigurasi addon")
if not prompt:
raise ValueError("Prompt tidak boleh kosong")
# ── 4. Siapkan request ke OpenAI ─────────────────────────────────────────
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": max_tokens
}
# ── 5. Kirim request & proses response ───────────────────────────────────
try:
response = requests.post(
"https://api.openai.com/v1/chat/completions",
headers=headers,
json=payload,
timeout=60 # 60 detik timeout
)
response.raise_for_status() # Raise exception jika status 4xx/5xx
result = response.json()
ai_response = result["choices"][0]["message"]["content"]
tokens_used = result.get("usage", {})
# ── 6. Return output sesuai contract ─────────────────────────────────
return {
"status": "ok",
"message": "ChatGPT response received successfully",
"data": {
"ai_output": ai_response,
"model_used": model,
"tokens_used": tokens_used
},
"runkey": ctx.get("runkey")
}
except requests.exceptions.Timeout:
raise Exception("Request ke ChatGPT API timeout (>60 detik)")
except requests.exceptions.HTTPError as e:
raise Exception(f"HTTP Error dari OpenAI: {str(e)}")
except requests.exceptions.RequestException as e:
raise Exception(f"Gagal menghubungi ChatGPT API: {str(e)}")
except (KeyError, IndexError) as e:
raise Exception(f"Format response OpenAI tidak terduga: {str(e)}")
# ── Entry point: dipanggil oleh platform Alurkerja ───────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="ChatGPT API Integration Script")
parser.add_argument(
"--run",
required=True,
help="Execution context: path ke file JSON atau JSON string langsung"
)
args = parser.parse_args()
# Load execution context
if args.run.endswith(".json"):
with open(args.run, 'r') as f:
ctx = json.load(f)
else:
ctx = json.loads(args.run)
# Eksekusi dan output ke STDOUT
try:
result = run(ctx)
print(json.dumps(result))
exit(0) # Success
except Exception as e:
error_output = {
"status": "error",
"message": str(e),
"runkey": ctx.get("runkey")
}
print(json.dumps(error_output))
exit(1) # Business error10. Lengkapi file index.json dengan lengkap dan pastikan semuanya sesuai dengan project views dan lain-lain.
{
"name": "ChatGPT-API-Integration",
"addon_key": "chatgpt-api-integration",
"version": "1.0.0",
"description": "ChatGPT API integration addon untuk Alurkerja platform.",
"author": "NamaAnda",
"license": "MIT",
"type": "REST",
"view_type": "MFE",
"view_path": "views/dist",
"component_scope": "chatgpt_view",
"custom_views": {
"CONFIGS": {
"CREATE": "chat_gpt_config",
"EDIT": "chat_gpt_config"
},
"ACTIONS": {
"sendCompletionRequest": "ask_chatgpt_view"
}
},
"config": {},
"scripts": {
"sendCompletionRequest": {
"description": "Script untuk mengirim request ke ChatGPT API dan mendapat response AI.",
"scripts": {
"type": "python",
"executable": "scripts/send_completion_request/send_completion_request.py"
}
}
},
"actions": {
"sendCompletionRequest": {
"name": "Ask ChatGPT",
"description": "Mengirim pertanyaan ke ChatGPT dan mendapat response AI.",
"type": "SCRIPT",
"endpoint": "sendCompletionRequest"
}
}
}11. Publish Addon
- Build frontend dulu
→ npm run build
→ cd ..- Publish ke AlurKerja
→ npx alurkerja-cli addon publish12. Cara Verifikasi Publish berhasil
- Masuk file .bpmn cari service task dan masuk ke setting
- Pilih Add Integration
- Pilih ChatGPT API
13. Mengeset Camunda Variables (dari React & Python)
Setelah view atau action selesai dieksekusi, nilai yang dihasilkan biasanya perlu disimpan sebagai process variable Camunda — supaya bisa dipakai di langkah BPMN berikutnya (gateway condition, default value form, notifikasi, dst.). Ada dua jalur set variable yang umum, tergantung dari mana nilainya berasal.
User Task A ──► variables Camunda ──► Service Task (Python) ──► variables Camunda ──► User Task B
(form) (form.setValue) (return dict) (alurkerjaParams)13.1 Dari React (Frontend MFE)
Sisi MFE memerlukan dua arah: membaca process variable yang sudah ada (untuk ditampilkan/diolah), dan menulis nilai baru yang akan disimpan saat User Task di-complete.
13.1.1 Membaca process variable
Platform meng-inject process variable ke komponen MFE via prop alurkerjaParams. Tergantung konteks (View Detail, Action, Form, dst.), variable bisa tiba di sumber yang berbeda. Pakai pola 3-source fallback — coba dari sumber paling spesifik dulu, fallback ke yang lain bila kosong:
import React from "react";
import { AlurkerjaMfeProps } from "./type/AlurkerjaType";
export default function TopTasksCard({ form, alurkerjaParams }: AlurkerjaMfeProps) {
// 3-source fallback — coba berurutan
const raw =
alurkerjaParams?.top_tasks // 1. inject langsung di top-level
?? alurkerjaParams?.processVariables?.top_tasks // 2. di bawah processVariables
?? form?.getValues?.("top_tasks"); // 3. dari form state lokal
const topTasks = typeof raw === "string" ? JSON.parse(raw) : raw ?? [];
return <ul>{topTasks.map((t: any) => <li key={t.id}>{t.name}</li>)}</ul>;
}Pola ini dipakai di komponen RefreshProgressButton / TaskProgressTable pada addon Daily Monitoring — tahan terhadap variasi cara platform menyuplai variable di tiap konteks komponen.
13.1.2 Menulis process variable
Gunakan form.setValue(name, value) dari React Hook Form yang disuplai platform via AlurkerjaMfeProps. Saat User Task di-complete oleh user, seluruh field di form akan diserialisasi menjadi process variable Camunda.
import React from "react";
import { AlurkerjaMfeProps } from "./type/AlurkerjaType";
export default function ApprovalButton({ form, alurkerjaParams }: AlurkerjaMfeProps) {
const handleApprove = () => {
// ⬇️ Set process variable yang akan masuk ke Camunda saat Complete Task
form?.setValue?.("approval_status", "approved");
form?.setValue?.("approval_timestamp", new Date().toISOString());
// Tombol Complete Task sendiri ditangani oleh platform
};
return (
<button type="button" onClick={handleApprove}>
Approve
</button>
);
}Tipe nilai boleh string / number / boolean / object / array — object dan array akan diserialisasi ke JSON saat dikirim ke Camunda.
Batas VARCHAR(4000) Camunda. Process variable string dipersist Camunda di kolom VARCHAR(4000). Untuk nilai panjang (HTML rich-text, JSON besar, log) sanitize / kompress dulu sebelum setValue — kalau tidak, persist akan gagal/terpotong di runtime.
- HTML: strip
style/class/idinline, atau pakai sanitizer library. Pola ini dipakaiRichTextViewpada addon Daily Monitoring agar muat dalam 4000 karakter. - JSON: simpan minimal field yang dibutuhkan; jangan dump full response API.
- Bila tetap > 4000 karakter: simpan blob ke storage eksternal (MinIO/S3) dan persist URL/reference-nya saja sebagai process variable.
Catatan tambahan:
setValuehanya menulis ke React Hook Form state — variable baru benar-benar masuk Camunda saat user mengklik tombol Complete Task.- Untuk update real-time di tengah task (mis. tombol Refresh yang fetch data dari API lalu menulis hasilnya ke field form), kombinasikan
fetch(...)+form.setValue(...)— lihat sub-section berikut tentang pola auth & tenant.
13.1.3 Memanggil API AlurKerja dari MFE (auth & tenant)
Saat MFE perlu memanggil API AlurKerja sendiri (mis. trigger action addon dari tombol Refresh), platform sudah meng-inject base URL, bearer token user, dan tenant aktif ke prop alurkerjaParams. Pakai langsung — tidak perlu sniff dari network response, tidak perlu hardcode di config addon.
Field alurkerjaParams | Isi | Untuk |
|---|---|---|
apiBaseUrl | Base URL platform aktif | Base URL fetch |
token | Bearer token user yang sedang login (auto, session-aware) | Header Authorization: Bearer ... |
activeTenant | ID tenant aktif user (auto, workspace-aware) | Header x-active-tenant |
taskId | ID User Task aktif | Reference / scoping |
probis | Info proses bisnis aktif | Reference |
Contoh lengkap (pola ini dipakai di RefreshProgressButton.tsx pada addon Daily Monitoring):
import React from "react";
import { AlurkerjaMfeProps } from "./type/AlurkerjaType";
export default function RefreshButton({ form, alurkerjaParams }: AlurkerjaMfeProps) {
const handleRefresh = async () => {
const baseUrl = alurkerjaParams?.apiBaseUrl ?? "";
const token = alurkerjaParams?.token ?? "";
const tenant = alurkerjaParams?.activeTenant ?? "";
const res = await fetch(
`${baseUrl}/api/v1/integration/addons/<addon-key>/<addon-id>/api/<action>`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-active-tenant": tenant,
},
body: JSON.stringify({ variables: { /* input untuk action */ } }),
}
);
const { data } = await res.json();
// Update field di form → akan tersimpan saat task complete
form?.setValue?.("progress_summary", JSON.stringify(data.progress_summary));
};
return (
<button type="button" onClick={handleRefresh}>
↻ Refresh Progress
</button>
);
}Jangan campur dua tipe token. Membedakan token untuk API AlurKerja sendiri vs token sistem eksternal sering jadi sumber bug:
| Kebutuhan | Sumber | Contoh use case |
|---|---|---|
Token API AlurKerja sendiri (/api/v1/integration/addons/...) | alurkerjaParams.token (auto — user session) | Trigger action addon, refresh data dari script addon Anda |
| Token sistem eksternal (ActiveCollab, Odoo, ChatGPT, dll.) | Field di ConfigView (value.*) → di Python via ctx["configuration"]["..."] | Memanggil API pihak ketiga dari Service Task Python |
Token AlurKerja tidak boleh disimpan di config — token user dinamis dan punya masa berlaku, kalau di-cache akan basi. Token eksternal harus di config — diisi admin sekali via ConfigView, dipakai script Python di sisi backend.
13.2 Dari Backend Python (Service Task / Script)
Setiap script Python yang mengimplementasi run(ctx) mengembalikan dict JSON. Semua key di top-level dict di-merge ke variables Camunda sesuai Output Contract di scripts/README.MD (Section 4). Field metadata standar (status, message, runkey) umumnya dipakai platform untuk handling response — pisahkan data bisnis Anda di nama key yang lain agar tidak bentrok.
#!/usr/bin/env python3
import json
import argparse
def run(ctx):
# Baca variabel input dari Camunda
variables = ctx.get("variables", {}) # dapat dimodifikasi via return
parameters = ctx.get("parameters", {}) # read-only
configuration = ctx.get("configuration", {}) # read-only
runkey = ctx.get("runkey", "")
top_tasks = variables.get("top_tasks", [])
# ... lakukan logika bisnis ...
progress_summary = compute_progress(top_tasks)
tasks_completed_today = sum(1 for t in progress_summary if t["completed_today"])
# ⬇️ Set process variables lewat return value
return {
"status": "ok",
"message": f"{tasks_completed_today} task selesai hari ini",
"runkey": runkey,
# ↓ field di bawah ini akan di-merge ke variables Camunda
"progress_summary": progress_summary,
"tasks_completed_today": tasks_completed_today,
"last_check_at": "2025-01-15T16:00:00Z",
}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--run", required=True)
args = parser.parse_args()
ctx = json.loads(args.run) if args.run.startswith("{") else json.load(open(args.run))
print(json.dumps(run(ctx)))Setelah Service Task selesai, di langkah BPMN berikutnya variable progress_summary, tasks_completed_today, dan last_check_at sudah tersedia di variables.
Alternatif: nest di bawah data. Untuk respons API multi-field yang sebaiknya tetap terkelompok, beberapa addon menempatkan hasil di bawah field data:
return {
"status": "ok",
"message": "ChatGPT response received",
"data": {
"ai_output": ai_response,
"tokens_used": result["usage"]["total_tokens"],
},
"runkey": runkey,
}Cara akses di langkah berikutnya: variables.data.ai_output dan variables.data.tokens_used.
Aturan penting (sesuai scripts/README.MD Section 4.2):
| Tindakan | Boleh? |
|---|---|
Action mengembalikan data untuk di-merge ke variables | ✅ |
Action mengubah parameters (dianggap input dari user/trigger) | ❌ |
Action mengubah configuration (konfigurasi addon) | ❌ |
| Action menyimpan state internal di filesystem/global | ❌ (stateless) |
Membaca config addon — value.* di React vs configuration di Python
Field config addon yang diisi admin lewat ConfigView (React) tersimpan di namespace value.* — mis. value.api_token, value.ac_base_url. Sisi React me-register field dengan prefix value.. Di Python, platform meneruskan field-field tersebut ke ctx["configuration"] tanpa prefix:
// ConfigView.tsx (React) — register di namespace value.*
<input {...form.register("value.api_token")} type="password" />
<input {...form.register("value.ac_base_url")} /># script Python — akses tanpa prefix value.
token = ctx["configuration"]["api_token"]
url = ctx["configuration"]["ac_base_url"]Jangan pakai prefix value. saat akses di Python — platform sudah men-strip prefix tersebut ketika men-serialisasi config ke execution context.
13.3 Membaca Variable di Langkah Berikutnya
Setelah variable di-set lewat salah satu jalur di atas, langkah BPMN berikutnya dapat membacanya dari konteks masing-masing:
| Konteks | Cara akses variable |
|---|---|
| Form Builder (Studio — default/visibility/validation) | ${variables.nama_variable} |
MFE View (React, AlurkerjaMfeProps) | 3-source fallback: alurkerjaParams?.nama_variable → alurkerjaParams?.processVariables?.nama_variable → form.getValues("nama_variable") — lihat 13.1.1 |
Python script (run(ctx)) | ctx.get("variables", {}).get("nama_variable") |
| BPMN Gateway (FEEL expression) | tasks_completed_today >= 3 atau progress_summary != null |
| Notifikasi / template (Email, WhatsApp, dll.) | {{ variables.nama_variable }} |
13.4 Memilih Jalur yang Tepat
| Use case | React (setValue) | Python (return) |
|---|---|---|
| Tombol approval / decision oleh user | ✓ | — |
| Hitung total dari field lain di form | ✓ | — |
| Update field saat user mengetik | ✓ | — |
| Refresh data dari API eksternal di tengah task (interaktif) | ✓ (fetch + setValue) | — |
| Sinkronisasi otomatis dengan sistem eksternal (Service Task) | — | ✓ |
| Validasi / komputasi server-side yang butuh kredensial | — | ✓ |
| Output yang harus konsisten lintas tenant (tidak bergantung browser user) | — | ✓ |
