diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 00000000..190edd7d --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,357 @@ +## Camunda 8 JS SDK for Node.js Quickstart + +From 8.5.0, the official [Camunda 8 JS SDK for Node.js](https://github.com/camunda/camunda-8-js-sdk) is available via [NPM](https://www.npmjs.com/package/@camunda8/sdk). + +It is written in TypeScript and has full type support for IDEs and editors that support intellisense. It can be used in JavaScript or TypeScript projects. + +It requires Node.js as a runtime environment. It cannot be used in a web browser for a number of [technical reasons](https://github.com/camunda/camunda-8-js-sdk/issues/79). + +## Quickstart + +A complete working version of the Quickstart code is available on GitHub [here](https://github.com/camunda-community-hub/c8-sdk-demo). + +- Create a new Node.js project that uses TypeScript: + +```bash +npm init -y +npm install -D typescript +npx tsc --init +``` + +- Install the SDK as a dependency: + +```bash +npm i @camunda8/sdk +``` + +## Connection configuration + +You have two choices: + +- explicit configuration in code +- zero-configuration constructor with environment variables + +The best way to do the configuration is via the zero-configuration constructor in code, and all values for configuration supplied via environment variables. This allows you to test the same code against different environments with no code changes. + +The environment variables that you need to set are the following (replace with your secrets and urls): + +### Self-Managed configuration + +```bash +# Self-Managed +export ZEEBE_ADDRESS='localhost:26500' +export ZEEBE_CLIENT_ID='zeebe' +export ZEEBE_CLIENT_SECRET='zecret' +export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' +export CAMUNDA_TASKLIST_BASE_URL='http://localhost:8082' +export CAMUNDA_OPERATE_BASE_URL='http://localhost:8081' +export CAMUNDA_OPTIMIZE_BASE_URL='http://localhost:8083' +export CAMUNDA_MODELER_BASE_URL='http://localhost:8070/api' +``` + +If you are running with multi-tenancy enabled: + +``` +export CAMUNDA_TENANT_ID='' +``` + +If your installation does not have TLS on Zeebe: + +``` +export CAMUNDA_SECURE_CONNECTION=false +``` + +### Camunda SaaS configuration + +```bash +export ZEEBE_ADDRESS='5c34c0a7-...-125615f7a9b9.syd-1.zeebe.camunda.io:443' +export ZEEBE_CLIENT_ID='yvvURO...' +export ZEEBE_CLIENT_SECRET='iJJu-SHg...' +export CAMUNDA_TASKLIST_BASE_URL='https://syd-1.tasklist.camunda.io/5c34c0a7-...-125615f7a9b9' +export CAMUNDA_OPTIMIZE_BASE_URL='https://syd-1.optimize.camunda.io/5c34c0a7-...-125615f7a9b9' +export CAMUNDA_OPERATE_BASE_URL='https://syd-1.operate.camunda.io/5c34c0a7-...-125615f7a9b9' +export CAMUNDA_OAUTH_URL='https://login.cloud.camunda.io/oauth/token' +export CAMUNDA_SECURE_CONNECTION=true +``` + +If you want to set these explicitly in code, the `Camunda8` constructor takes these values, with the same key names, in the constructor. + +### Using the SDK + +- Create a file `index.ts` in your IDE. +- Import the SDK: + +```typescript +import { Camunda8 } from '@camunda8/sdk' +import path from 'path' // we'll use this later + +const camunda = new Camunda8() +``` + +- Get a Zeebe GRPC API client. This is used to deploy process models and start process instances. + +```typescript +const zeebe = camunda.getZeebeGrpcApiClient() +``` + +- Get an Operate client. This is used to interact with completed processes and deployed process models. + +```typescript +const operate = camunda.getOperateApiClient() +``` + +- Get a Tasklist client. This is used to interact programatically with user tasks. + +```typescript +const tasklist = camunda.getTasklistApiClient() +``` + +### Deploy a process model + +- We will deploy a process model (we'll create the model in a moment). Network operations are asynchronous and methods that operate over the network return Promises, so we will wrap the main function of the program in an `async` function: + +```typescript +async function main() { + const deploy = await zeebe.deployResource({ + processFilename: path.join(process.cwd(), 'process.bpmn'), + }) + console.log( + `[Zeebe] Deployed process ${deploy.deployments[0].process.bpmnProcessId}` + ) +} + +main() // remember to invoke the function +``` + +- Paste the process model XML below into a file called `process.bpmn`: + +```xml + + + + + Flow_0yqo0wz + + + + + Flow_03qgl0x + + + + + + + Flow_0yqo0wz + Flow_0qugen1 + + + Flow_0qugen1 + Flow_03qgl0x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +- This is the model we are using: + +![process model](./img/process-model.png) + +- You can run the program now, and see the process model deploy to Camunda: + +```bash +npx ts-node index.ts +``` + +If your configuration is correct, you will see the following: + +``` +Deployed process c8-sdk-demo +``` + +### Create a service worker + +- Outside the main function, add the following code: + +```typescript +console.log('Starting worker...') +zbc.createWorker({ + taskType: 'service-task', + taskHandler: (job) => { + console.log(`[Zeebe Worker] handling job of type ${job.type}`) + return job.complete({ + serviceTaskOutcome: 'We did it!', + }) + }, +}) +``` + +This will start a service task worker that runs in an asynchronous loop, invoking the `taskHandler` function whenever a job for the service task type `service-task` is available. + +The handler must return a job completion function - one of `fail`, `complete` or `forward`. This is enforced by the type system and ensures that you do not write code that does not have code paths that do not respond to Zeebe after taking a job. The `job.complete` function can take an object that represents variables to update. + +### Create a programatic human task worker + +Our process has a human task after the service task. The service task worker will complete the service task job, and we will complete the human task using the Tasklist API client. + +- Add the following code beneath the service worker code: + +```typescript +console.log(`Starting human task poller...`) +setInterval(async () => { + const res = await tasklist.searchTasks({ + state: 'CREATED', + }) + if (res.length > 0) { + console.log(`[Tasklist] fetched ${res.length} human tasks`) + res.forEach(async (task) => { + console.log( + `[Tasklist] claiming task ${task.id} from process ${task.processInstanceKey}` + ) + const t = await tasklist.assignTask({ + taskId: task.id, + assignee: 'demobot', + allowOverrideAssignment: true, + }) + console.log( + `[Tasklist] servicing human task ${t.id} from process ${t.processInstanceKey}` + ) + await tasklist.completeTask(t.id, { + humanTaskStatus: 'Got done', + }) + }) + } else { + console.log('No human tasks found') + } +}, 3000) +``` + +We now have an asynchronously polling service worker and an asynchronously polling human task worker. + +The last step is to create a process instance. + +### Create a process instance + +There are two options for creating a process instance. + +For long-running processes, you will use `createProcessInstance`, which returns as soon as the process instance is created with the process instance id. + +For the shorter-running process that we are using, we will use `createProcessInstanceWithResult`, which awaits the completion of the process and returns with the final variable values. + +- Locate the following line in the `main` function: + +```typescript +console.log( + `[Zeebe] Deployed process ${res.deployments[0].process.bpmnProcessId}` +) +``` + +- Directly after that, inside the `main` function, add the following: + +```typescript +const p = await zbc.createProcessInstanceWithResult({ + bpmnProcessId: `c8-sdk-demo`, + variables: { + humanTaskStatus: 'Needs doing', + }, +}) +console.log(`[Zeebe] Finished Process Instance ${p.processInstanceKey}`) +console.log(`[Zeebe] humanTaskStatus is "${p.variables.humanTaskStatus}"`) +console.log(`[Zeebe] serviceTaskOutcome is "${p.variables.serviceTaskOutcome}"`) +``` + +Run the program with the following command: + +```bash +npx ts-node index.ts +``` + +You should see output similar to the following: + +``` +Creating worker... +Starting human task poller... +[Zeebe] Deployed process c8-sdk-demo +[Zeebe Worker] handling job of type service-task +[Tasklist] fetched 1 human tasks +[Tasklist] claiming task 2251799814895765 from process 2251799814900881 +[Tasklist] servicing human task 2251799814895765 from process 2251799814900881 +[Zeebe] Finished Process Instance 2251799814900881 +[Zeebe] humanTaskStatus is "Got done" +[Zeebe] serviceTaskOutcome is "We did it!" +``` + +The program will continue running until you hit Ctrl-C. This is because both the service worker and the task poller that we wrote are running in continuous loops. + +There are a few more things we will do to explore the functionality of the SDK. + +### Retrieve a process instance + +When you create a process instance that runs for some time, you will many times do it by creating a process with `createProcessInstance` and getting back the process instance key of the running process, rather than waiting for it to complete. + +To examine the process instance status, you can use the process instance key to query the Operate API. You can also examine process instances after they complete in the same way. We'll do that with the process instance that we created, after it completes. + +- Locate the following line in the `main` function: + +```typescript +console.log(`[Zeebe] serviceTaskOutcome is "${p.variables.serviceTaskOutcome}"`) +``` + +After that line, inside the `main` function add the following: + +```typescript +const historicalProcessInstance = await operate.getProcessInstance( + p.processInstanceKey +) +console.log('[Operate]', historicalProcessInstance) +``` + +When you run the program now, you will see additional output similar to the following: + +``` +{ + key: 2251799814905817, + processVersion: 1, + bpmnProcessId: 'c8-sdk-demo', + startDate: '2024-04-08T09:11:06.157+0000', + endDate: '2024-04-08T09:11:12.403+0000', + state: 'COMPLETED', + processDefinitionKey: 2251799814900879, +} +``` + +The state may be `ACTIVE` rather than `COMPLETED`. This occurs because the data read over the Operate API is historical data from the Zeebe exporter, and lags behind the actual state of the system. It is _eventually consistent_. + +## Further steps + +Consult the complete API documentation for the SDK [here](https://camunda.github.io/camunda-8-js-sdk/). diff --git a/img/process-model.png b/img/process-model.png new file mode 100644 index 00000000..66be8ed9 Binary files /dev/null and b/img/process-model.png differ