راهنمای استفاده (Usage Guide)
قابلیتهای کلیدی موتور گردش کار:
۱. شروع فرایند جدید
۲. فهرستگیری از اجراهای یک فرایند (processKey)
۳. ارجاع کار به کاربر، گروه، یا چند نفر همزمان
۴. کارتابل وظایف باز
۵. بررسی وضعیت تکمیل ارجاعهای چندنفره
۶. تکمیل وظیفه و بستن کامل پرونده
۷. دریافت آمار و فهرست فرایندهای مرتبط با یک کاربر
۸. رفتن خودکار به مرحله بعد با onAllCompleted (بعد از join موازی)
dotnet run --project src/TaskFlow.Serverمستندات تعاملی Swagger: http://127.0.0.1:8081/swagger
| عملیات | نحوهٔ فراخوانی (REST API) |
|---|---|
| شروع فرایند | POST /v1/processes/start با بدنهٔ { "processKey", "initiator", "parameters?" } |
| لیست اجراها | GET /v1/processes/{processKey}/instances |
| ارجاع کار | POST /v1/assignments با بدنهٔ { "definitionKey", "parentInstanceId?", "to" } |
| کارتابل وظایف | GET /v1/tasks?user=mortenaho یا GET /v1/tasks?group=legal |
| تحویل گرفتن تسک (Claim) | POST /v1/tasks/{id}/claim |
| لغو تحویل تسک (Unclaim) | POST /v1/tasks/{id}/unclaim |
| وضعیت تکمیل چندنفره | GET /v1/instances/{id}/completion |
| تکمیل وظیفه | POST /v1/tasks/{id}/complete |
| تکمیل و پایان فرایند | POST /v1/tasks/{id}/complete-and-end |
| فرایندهای کاربر | GET /v1/users/{user}/processes با فیلتر اختیاری state=open، closed یا notStarted |
برای میکروسرویسها مسیر پیشنهادی SDK کلاینت (TaskFlow.Client) است: یک بار TaskFlow.Server را بالا میآورید و هر سرویس فقط با HTTP به همان آدرس وصل میشود. در تستهای واحد یا وقتی خودتان میزبان انجین هستید، میتوانید Engine را داخلپردازشی هم بسازید. نمونهٔ curl: examples/curl.sh.
۱. مفاهیم پایه
| واژه | توضیحات و مفهوم |
|---|---|
processKey | کلید شناسهٔ نوع فرایند (مانند purchase یا employeeTermination) |
definitionKey | کلید تعریف ثبتشده برای فرایند که در خروجی متد شروع بازگردانده میشود |
instanceId | شناسهٔ یک نمونهٔ اجرایی؛ فراخوانی Start نمونهٔ ریشه را میسازد و هر ارجاع (AssignTo) یک نمونهٔ جدید ایجاد میکند |
Task | رکورد وظیفه در کارتابل که به یک کاربر یا یک گروه تخصیص یافته است |
Inbox / Tasks | وظایف در وضعیت باز (open) متعلق به یک کاربر (شامل وظایف فردی و گروههای عضو) یا یک گروه |
شناسهٔ انجامدهندهٔ عملیات از طریق هدر X-Actor-Id ارسال میگردد. این مقدار برای انجین یک شناسهٔ مات (opaque) است — مثلاً 102 بهعنوان شناسهٔ کاربر در سامانهٔ خودتان. انجین جدول کاربران ندارد و لاگین نمیکند؛ فقط همان رشته را در تسکها ذخیره و با آن مجوز عملیات را میسنجد.
اگر WF_USERS و WF_GROUP_* تنظیم نشوند، سرویس بهصورت پیشفرض از OpenDirectory استفاده میکند: هر شناسهٔ کاربر یا گروه پذیرفته میشود و عضویت گروه اجباری نیست. وقتی فهرست کاربران/گروهها را تعریف کنید، StaticDirectory فعال میشود و ارجاع به گروه خالی یا ادعای عضویت نادرست رد میگردد. در پروداکشن میتوانید بهجای آنها IDirectory را به SSO / LDAP وصل کنید.
۲. استفاده از طریق SDK در زبان C#
هدف این بخش: یک TaskFlow.Server از قبل بالا آمده (مثلاً در Docker یا کلاستر) و چند میکروسرویس که هر کدام فقط پکیج TaskFlow.Client را دارند و به همان آدرس HTTP وصل میشوند. انجین، دیتابیس و IDirectory روی سرور میمانند؛ کلاینتها Engine یا Postgres را داخل سرویس خودشان نمیسازند.
flowchart RL
ms1["Ordering API"]
ms2["HR API"]
ms3["Legal BFF"]
eng["TaskFlow.Server"]
db[("Postgres")]
ms1 -->|"TaskFlow.Client"| eng
ms2 -->|"TaskFlow.Client"| eng
ms3 -->|"TaskFlow.Client"| eng
eng --> db
style eng fill:#C2E5FF,stroke:#3DADFF,color:#1a1a2e
style db fill:#CDF4D3,stroke:#66D575,color:#1a1a2e
style ms1 fill:#f3e5f5,stroke:#9c27b0,color:#1a1a2e
style ms2 fill:#f3e5f5,stroke:#9c27b0,color:#1a1a2e
style ms3 fill:#f3e5f5,stroke:#9c27b0,color:#1a1a2eپیشنیاز: سرور مشترک
dotnet run --project src/TaskFlow.Server
# یا: docker compose up --build
# Base URL پیشفرض: http://127.0.0.1:8081در پروداکشن روی سرور WF_API_KEYS را تنظیم کنید؛ هر میکروسرویس همان کلید را فقط در env بکاند خودش نگه میدارد و بهصورت X-API-Key میفرستد (نه در مرورگر).
افزودن پکیج به میکروسرویس
<ItemGroup>
<ProjectReference Include="..\..\path\to\TaskFlow.Client\TaskFlow.Client.csproj" />
<!-- یا پس از انتشار: <PackageReference Include="TaskFlow.Client" Version="..." /> -->
</ItemGroup>ثبت در Program.cs هر میکروسرویس
using TaskFlow.Client;
builder.Services.AddTaskFlowClient(o =>
{
o.BaseAddress = new Uri(builder.Configuration["TaskFlow:BaseUrl"]
?? "http://taskflow:8081/");
o.ApiKey = builder.Configuration["TaskFlow:ApiKey"]; // همان WF_API_KEYS سرور
o.TenantId = builder.Configuration["TaskFlow:TenantId"]; // اختیاری؛ مثلاً acme
});
// بعداً در کنترلر / هندلر:
// private readonly TaskFlowClient _tf;
// private readonly TaskFlowOrchestrator _orch;بدون DI هم میتوانید بسازید:
using TaskFlow.Client;
var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8081/") };
var tf = new TaskFlowClient(http, new TaskFlowClientOptions
{
ApiKey = Environment.GetEnvironmentVariable("TASKFLOW_API_KEY"),
TenantId = "default",
});
await tf.EnsureHealthy();شروع فرایند
خروجی شامل DefinitionKey و InstanceId است. هدر X-Actor-Id را SDK از آرگومان initiator / actor میگذارد.
using TaskFlow.Application;
using TaskFlow.Client;
var started = await tf.Start(
"purchase",
"sara",
new Dictionary<string, object?> { ["amount"] = 1.5e8 });ارجاع به کاربر یا گروه
ToKind میتواند user، group یا users باشد (AssigneeKind). برای حالت چندنفره از ToIds استفاده کنید.
using TaskFlow.Domain;
var refer = await tf.AssignTo("sara", new AssignToInput
{
DefinitionKey = started.DefinitionKey,
ParentInstanceId = started.InstanceId,
Title = "Legal review",
ToKind = AssigneeKind.User,
ToId = "mortenaho",
// ToIds = ["mortenaho", "tina"], // AssigneeKind.Users
});تخصیص موازی
مرحله بعد را با onAllCompleted روی همان AssignTo تعریف کنید. هر کاربر فقط CompleteTask میزند؛ بعد از آخرین تکمیل، ParallelJoinHandler روی TaskCompleted ارجاع بعدی را میسازد.
flowchart TD
start([Start]) --> parallel[AssignTo users]
parallel --> m[mortenaho]
parallel --> c[tina]
m --> join{all done?}
c --> join
join -->|no| wait[open tasks remain]
join -->|yes| next[AssignTo onAllCompleted]- هر
AssignToیک instance فرزند میسازد؛AllCompletedفقط برای همان فرزند است. DefinitionKeyوParentInstanceIdدرonAllCompletedرا میتوان خالی گذاشت.
نمونه
var parallel = await tf.AssignTo("sara", new AssignToInput
{
DefinitionKey = started.DefinitionKey,
ParentInstanceId = started.InstanceId,
Title = "تأیید موازی",
ToKind = AssigneeKind.Users,
ToIds = ["mortenaho", "tina"],
OnAllCompleted = new AssignToInput
{
Title = "بررسی حقوقی",
ToKind = AssigneeKind.Group,
ToId = "legal",
},
});
foreach (var task in parallel.Tasks)
await tf.CompleteTask(task.Id, task.AssigneeId, "تأیید شد");
var legalInbox = await tf.PendingTasks(group: "legal");REST معادل:
curl -s -X POST http://127.0.0.1:8081/v1/assignments \
-H 'Content-Type: application/json' -H 'X-Actor-Id: sara' \
-d '{
"definitionKey": "purchase",
"parentInstanceId": "INSTANCE",
"from": "sara",
"title": "تأیید موازی",
"join": "all",
"to": { "kind": "users", "ids": ["mortenaho", "tina"] },
"onAllCompleted": {
"title": "بررسی حقوقی",
"to": { "kind": "group", "id": "legal" }
}
}'BFF
یک endpoint برای approve کافی است؛ taskId تفاوت میکند، handler یکی است. BFF فقط CompleteTask میزند — join با انجین است.
[HttpPost("tasks/{taskId}/approve")]
public async Task<IActionResult> Approve(string taskId, [FromBody] ApproveRequest body)
{
var userId = User.FindFirst("sub")?.Value ?? "";
await _tf.CompleteTask(taskId, userId, body.Note ?? "تأیید شد");
return Ok();
}برای برگرداندن approved / waiting_for_others / all_done در همان پاسخ: CompleteTaskWithOutcome و فیلد StatusKey.
ثبت کلاینت:
builder.Services.AddTaskFlowClient(o =>
{
o.BaseAddress = new Uri("http://taskflow:8080/");
o.ApiKey = builder.Configuration["TaskFlow:ApiKey"];
});
ProcessOrchestrator/CompleteAndAssignToبرای callback دستی قدیمی است.
نمونه in-process: examples/Scenario.
اپ کامل با UI: employee-termination-app (Next.js BFF، همان الگوی بالا).
| مفهوم | معنی |
|---|---|
Users + ToIds | چند تسک موازی؛ همه باید done شوند |
onAllCompleted | مرحله بعد بعد از join |
CompleteTask | بستن تسک؛ join در انجین |
CompleteTaskWithOutcome | همان + StatusKey برای پاسخ HTTP |
CompleteAndAssignTo | قدیمی |
کارتابل، تکمیل، و فرایندهای کاربر
var inbox = await tf.PendingTasks(user: "mortenaho");
var groupInbox = await tf.PendingTasks(group: "legal");
var ended = await tf.CompleteAndEnd(refer.Task!.Id, "mortenaho", "Case closed");
// ended.Process.Status == "completed"
var mine = await tf.ListUserProcesses("sara");
var open = await tf.ListUserProcesses("sara", state: "open");سایر متدهای مهم SDK: ClaimTask / UnclaimTask، Completion(instanceId)، ListByProcessKey، GetInstance، RegisterDefinition.
چند سرویس، یک tenant یا چند tenant
| سناریو | تنظیم |
|---|---|
| همهٔ میکروسرویسها یک سازمان | TenantId را خالی بگذارید یا همه default |
| جداسازی داده بین مشتریها | در هر سرویس o.TenantId = "acme" یا در هر فراخوانی آرگومان tenantId |
| محیط امن | همان ApiKey مشترک بکاندها؛ React کلید را نمیبیند |
حالت توکار (فقط وقتی خودتان سرور را میسازید)
اگر میخواهید در تست یا یک باینری واحد، انجین را داخل همان پروسس داشته باشید (نه اتصال به سرور ریموت)، هنوز میتوانید Engine + MemoryStore/PostgresStore بسازید. دایرکتوری کاربران (IDirectory) فقط روی میزبان سرور معنی دارد، نه روی میکروسرویس کلاینت:
using TaskFlow.Application;
using TaskFlow.Infrastructure;
var dir = new StaticDirectory(
["sara", "mortenaho", "tina", "hamid"],
new Dictionary<string, IReadOnlyList<string>>
{
["legal"] = ["mortenaho", "tina"],
["finance"] = ["hamid", "tina"],
});
var eng = new Engine(new MemoryStore(), dir);
var orch = new ProcessOrchestrator(eng);برای اتصال سرور به SSO / LDAP، IDirectory را روی همان پروسس TaskFlow.Server پیاده کنید (نه داخل هر میکروسرویس). نمونهٔ رابط:
public interface IDirectory
{
bool EnforcesMembership { get; }
Task<bool> UserExists(string userId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<string>> GroupMembers(string groupId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<string>> UserGroups(string userId, CancellationToken cancellationToken = default);
Task<bool> IsMember(string userId, string groupId, CancellationToken cancellationToken = default);
}جزئیات معماری کلید API و BFF: architecture.md.
۳. راهنمای وبسرویس REST API
آدرس پایه: http://localhost:8081.
هدرهای اصلی:
X-Actor-Id: شناسهٔ کاربر انجامدهندهٔ درخواست (اجباری در اکثر عملیات). این هدر لاگین نیست و انجین توکن صادر نمیکند.X-Tenant-Id: شناسهٔ سازمان جهت جداسازی دادهها (اختیاری؛ پیشفرض:default).X-API-Key: کلید مشترک سرویس برای بکاند یا API Gateway. در پروداکشن اجباری است (متغیرWF_API_KEYS). مسیرهای/healthو مستندات مستثنی هستند.
اتصال از React: کلید را در مرورگر نفرستید
X-API-Key برای curl، تست، و بکاند شما است، نه برای fetch در React. اگر فرانت کلید را در هدر بگذارد، در Network مرورگر لو میرود. معماری پیشنهادی و دیاگرامها: architecture.md — بخش ۶.
React (فقط API خودتان، بدون کلید انجین):
فراخوانی مستقیم انجین از مرورگر با X-API-Key اشتباه است — کلید در Network لو میرود.
await fetch("/api/inbox", { credentials: "include" });بکاند / BFF (اینجا کلید از env خوانده میشود):
userId از جلسهٔ لاگین اپ شما میآید، نه از query مرورگر. نمونهٔ Express؛ معادل آن در ASP.NET / Nest یکسان است.
app.get("/api/inbox", async (req, res) => {
const userId = req.session.userId;
const r = await fetch(`${process.env.TASKFLOW_URL}/v1/tasks?user=${encodeURIComponent(userId)}`, {
headers: {
"X-API-Key": process.env.WF_API_KEYS.split(",")[0],
"X-Actor-Id": userId,
},
});
res.status(r.status).json(await r.json());
});فلو خلاصه: کاربر در اپ شما لاگین میکند → React به /api/... همان اپ درخواست میزند → بکاند جلسه را چک میکند → با X-API-Key و X-Actor-Id به انجین میزند → پاسخ را به React برمیگرداند.
پیادهسازی مرجع: employee-termination-app (app/api/* + lib/infrastructure/engine).
۱. شروع فرایند (Start)
POST /v1/processes/start
{ "processKey": "purchase", "initiator": "sara", "parameters": { "amount": 150000000 } }
→ { "definitionKey": "purchase", "instanceId": "..." }در صورت خالی بودن فیلد initiator، مقدار هدر X-Actor-Id جایگزین میشود. اگر برای processKey تعریفی وجود نداشته باشد، بهصورت خودکار ساخته میشود.
یک processKey میتواند بارها شروع شود (برای نمونه فرایند employeeTermination برای هر کارمند بهطور مستقل اجرا میگردد):
GET /v1/processes/employeeTermination/instances{
"processKey": "employeeTermination",
"total": 2,
"instances": [
{
"instanceId": "...",
"initiator": "hr",
"status": "running",
"parameters": { "employeeId": "1002" },
"tasks": [ ... ],
"taskTotal": 1,
"tasksOpen": 1,
"allTasksCompleted": false
}
]
}فهرست فوق فقط نمونههای ریشه (Start) را بازمیگرداند؛ وظایف مربوط به ارجاعهای فرزند نیز در فیلد tasks همان نمونه لیست میشوند.
۲. ارجاع کار (Assignment)
parentInstanceId همان instanceId دریافتی از پاسخ Start است.
POST /v1/assignments
{
"definitionKey": "purchase",
"parentInstanceId": "<instanceId from start>",
"from": "sara",
"title": "Review",
"to": { "kind": "user", "id": "mortenaho" }
}
→ { "instanceId": "...", "definitionKey": "purchase", "task": { ... }, "tasks": [ ... ] }در صورت خالی بودن فیلد from، مقدار هدر X-Actor-Id درج میشود.
مقدار to.kind | فیلدهای مورد نیاز | نتیجه |
|---|---|---|
user | id | ایجاد یک تسک اختصاصی برای همان کاربر |
group | id | ایجاد یک تسک گروهی؛ کلیهٔ اعضای گروه آن را در کارتابل مشاهده میکنند |
users | ids | ایجاد یک تسک مجزا بهازای هر کاربر؛ وضعیت تکمیل کل ارجاع با شناسهٔ instanceId قابل رهگیری است |
۳. کارتابل وظایف (Tasks)
GET /v1/tasks?user=mortenaho— دریافت وظایف شخصی کاربرmortenahoبههمراه وظایف گروههایی که وی در آنها عضویت دارد.GET /v1/tasks?group=legal— صرفاً وظایف ارجاعشده به گروهlegal.
هر تسک دارای مشخصههای assigneeKind و assigneeId است تا مالکیت آن مشخص باشد.
۴. تحویل گرفتن وظیفهٔ گروهی (Claim)
تسکهای گروهی در ابتدا در وضعیت open قرار دارند و برای همهٔ اعضای گروه قابل مشاهده هستند. عضوی که میخواهد وظیفه را انجام دهد آن را رزرو میکند:
POST /v1/tasks/{id}/claim
{ "from": "mortenaho" }وضعیت تسک به claimed و فیلد claimedBy به mortenaho تغییر مییابد. در صورت تلاش عضو دیگر برای کلیم همزمان، خطای 409 Conflict بازگردانده شده و تسک از کارتابل سایر اعضا خارج میشود. پس از این مرحله، صرفاً کاربر mortenaho مجاز به تکمیل تسک خواهد بود.
با فراخوانی POST /v1/tasks/{id}/unclaim تسک مجدداً آزاد شده و به وضعیت open بازمیگردد.
تسکهای فردی نیازی به کلیم اجباری ندارند و مستقیماً قابل تکمیل هستند.
۵. بررسی وضعیت تکمیل چندنفره (Completion)
GET /v1/instances/{assignmentInstanceId}/completion{
"instanceId": "...",
"allCompleted": false,
"total": 3,
"completed": 1,
"open": 2,
"tasks": [ ... ]
}پاسخ متد POST /v1/tasks/{id}/complete نیز شامل فیلد completion است.
تکمیل معمولی صرفاً همان وظیفه (و در صورت اتمام همهٔ وظایف ارجاع، اینستنس ارجاع مربوطه) را میبندد؛ اما اینستنس ریشه در وضعیت running باقی میماند.
۶. تکمیل وظیفه و بستن کل فرایند (Complete and End)
مسیر POST /v1/tasks/{id}/complete-and-end با همان مجوزهای تکمیل وظیفه، اقدامات زیر را بهصورت یکپارچه انجام میدهد:
۱. وضعیت وظیفهٔ جاری را به done تغییر میدهد.
۲. سایر وظایف باز (open یا claimed) در کل درخت فرایند را به وضعیت cancelled منتقل کرده و از کارتابلها خارج میسازد.
۳. اینستنس ریشه و تمام ارجاعهای فرزند را در وضعیت completed قرار میدهد.
پس از این مرحله، ثبت هرگونه ارجاع جدید روی این فرایند با خطای 400 Bad Request مواجه خواهد شد.
۷. فرایندهای کاربر (User Processes)
متد GET /v1/users/sara/processes فهرست فرایندهایی که توسط کاربر sara ایجاد شدهاند را بازمیگرداند:
وضعیت (state) | توضیحات |
|---|---|
notStarted | فرایند شروع شده اما هنوز ارجاعی برای آن ثبت نشده است |
open | فرایند دارای ارجاع بوده و ریشه همچنان در وضعیت running قرار دارد |
closed | فرایند بهطور کامل خاتمه یافته (completed) است |
{
"user": "sara",
"open": 1,
"closed": 2,
"notStarted": 3,
"total": 6,
"instances": [ ... ]
}با ارسال پارامتر ?state=open، لیست خروجی بر اساس فرایندهای باز فیلتر میشود؛ در حالی که مقادیر آماری (open, closed, notStarted, total) همواره وضعیت کلی را گزارش میدهند.
۴. پیکربندی محیط و Docker
همهٔ تنظیمات اجرای سرویس از متغیرهای محیطی خوانده میشوند (کلاس AppComposition). مقداردهی از طریق .env، Docker، یا محیط سیستم کافی است؛ فایل تنظیمات جداگانه لازم نیست.
آدرس گوش دادن (Listen)
اگر ASPNETCORE_URLS تنظیم شده باشد، همان اولویت دارد و ADDR نادیده گرفته میشود. در غیر این صورت ADDR استفاده میشود (پیشفرض :8081).
- مقدارهایی مثل
:8081روی همهٔ اینترفیسها باز میشوند (http://0.0.0.0:8081). - میتوانید آدرس کامل هم بدهید، مثلاً
http://127.0.0.1:9000.
ذخیرهسازی
وضعیت DATABASE_URL | رفتار |
|---|---|
| خالی | دادهها در حافظه (MemoryStore) میمانند و با خاموش شدن سرویس پاک میشوند |
| تنظیمشده | اتصال به Postgres؛ اگر دیتابیس هنوز آماده نباشد، تا حدود ۳۰ ثانیه تلاش مجدد میکند |
فایل docker-compose.yml بهطور پیشفرض Postgres را بالا میآورد و DATABASE_URL را تنظیم میکند. جزئیات جداول: database.md.
کاربران و گروهها (Directory)
| وضعیت | رفتار |
|---|---|
نه WF_USERS و نه WF_GROUP_* | حالت باز (OpenDirectory): هر شناسهٔ غیرخالی کاربر/گروه پذیرفته میشود |
| حداقل یکی تنظیم شده | حالت ثابت (StaticDirectory): فقط همان کاربران و عضویتهای تعریفشده معتبرند |
مثال:
WF_USERS=sara,mortenaho,tina,hamid
WF_GROUP_legal=mortenaho,tina
WF_GROUP_finance=hamid,tinaنام گروه از پسوند متغیر گرفته میشود (WF_GROUP_legal → گروه legal) و به حروف کوچک نرمال میشود. مقادیر با ویرگول جدا میشوند.
کلید API
WF_API_KEYS یک یا چند کلید مشترک سرویس است (با ویرگول). این توکن لاگین کاربر نیست؛ فقط بکاند یا Gateway شما باید آن را با هدر X-API-Key یا Authorization: Bearer بفرستد. React و مرورگر نباید این کلید را ببینند.
محیط (ASPNETCORE_ENVIRONMENT) | WF_API_KEYS |
|---|---|
Development | اختیاری؛ خالی = احراز هویت API خاموش |
هر چیز دیگر (مثلاً Production) | اجباری؛ بدون آن سرویس اصلاً بالا نمیآید |
خلاصهٔ متغیرها
| متغیر | پیشفرض | نقش |
|---|---|---|
ADDR | :8081 | آدرس گوش دادن؛ فقط وقتی ASPNETCORE_URLS خالی است |
ASPNETCORE_URLS | خالی | اولویت بالاتر برای آدرس گوش دادن (استاندارد ASP.NET) |
DATABASE_URL | خالی | اتصال Postgres؛ خالی = حافظه موقت |
WF_USERS | خالی | فهرست کاربران (ویرگولجدا)؛ اگر کاربران یا گروهها تعریف شوند، حالت ثابت فعال میشود |
WF_GROUP_<id> | — | اعضای گروه id، مثلاً WF_GROUP_legal=a,b |
WF_API_KEYS | خالی | کلید(های) مشترک API؛ خارج از Development اجباری |
ASPNETCORE_ENVIRONMENT | بسته به اجرا | در Development میتوان بدون کلید کار کرد |
نمونهٔ اجرای محلی با Docker:
cp .env.example .env
docker compose up --build
curl -s http://localhost:8081/health
curl -s -H 'X-API-Key: local-dev-key' -H 'X-Actor-Id: sara' \
http://localhost:8081/v1/users/sara/processesنمونهٔ curl بالا برای تست از سرور/ترمینال است، نه برای کپی در فرانت.