flowable 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +21 -0
- data/README.md +872 -0
- data/bin/flowable +510 -0
- data/lib/flowable/flowable.rb +273 -0
- data/lib/flowable/resources/base.rb +44 -0
- data/lib/flowable/resources/bpmn_deployments.rb +90 -0
- data/lib/flowable/resources/bpmn_history.rb +228 -0
- data/lib/flowable/resources/case_definitions.rb +115 -0
- data/lib/flowable/resources/case_instances.rb +188 -0
- data/lib/flowable/resources/deployments.rb +87 -0
- data/lib/flowable/resources/executions.rb +134 -0
- data/lib/flowable/resources/history.rb +264 -0
- data/lib/flowable/resources/plan_item_instances.rb +131 -0
- data/lib/flowable/resources/process_definitions.rb +142 -0
- data/lib/flowable/resources/process_instances.rb +200 -0
- data/lib/flowable/resources/tasks.rb +281 -0
- data/lib/flowable/version.rb +37 -0
- data/lib/flowable/workflow.rb +444 -0
- data/lib/flowable.rb +9 -0
- data/lib/flowable_client/resources/bpmn_history.rb +228 -0
- data/lib/flowable_client/resources/process_definitions.rb +142 -0
- data/lib/flowable_client/resources/process_instances.rb +200 -0
- metadata +104 -0
data/README.md
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
# Flowable
|
|
2
|
+
|
|
3
|
+
[](https://www.ruby-lang.org/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.flowable.com/)
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
A comprehensive Ruby client for the [Flowable](https://www.flowable.com/) REST API, supporting both **CMMN** (Case Management) and **BPMN** (Business Process) engines.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Configuration](#configuration)
|
|
16
|
+
- [CMMN API](#cmmn-api)
|
|
17
|
+
- [Deployments](#deployments)
|
|
18
|
+
- [Case Definitions](#case-definitions)
|
|
19
|
+
- [Case Instances](#case-instances)
|
|
20
|
+
- [Tasks](#tasks)
|
|
21
|
+
- [Plan Item Instances](#plan-item-instances)
|
|
22
|
+
- [History](#history)
|
|
23
|
+
- [BPMN API](#bpmn-api)
|
|
24
|
+
- [BPMN Deployments](#bpmn-deployments)
|
|
25
|
+
- [Process Definitions](#process-definitions)
|
|
26
|
+
- [Process Instances](#process-instances)
|
|
27
|
+
- [BPMN Tasks](#bpmn-tasks)
|
|
28
|
+
- [Executions](#executions)
|
|
29
|
+
- [BPMN History](#bpmn-history)
|
|
30
|
+
- [Working with Variables](#working-with-variables)
|
|
31
|
+
- [Error Handling](#error-handling)
|
|
32
|
+
- [Pagination](#pagination)
|
|
33
|
+
- [CLI Tool](#cli-tool)
|
|
34
|
+
- [Workflow DSL](#workflow-dsl)
|
|
35
|
+
- [Testing](#testing)
|
|
36
|
+
- [Known Issues](#known-issues)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Add to your Gemfile:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
gem 'flowable'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or install locally:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
gem 'flowable', path: '/path/to/flowable'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require 'flowable'
|
|
58
|
+
|
|
59
|
+
# Initialize the client
|
|
60
|
+
client = Flowable::Client.new(
|
|
61
|
+
host: 'localhost',
|
|
62
|
+
port: 8080,
|
|
63
|
+
username: 'rest-admin',
|
|
64
|
+
password: 'test'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Deploy a CMMN case
|
|
68
|
+
deployment = client.deployments.create('my-case.cmmn')
|
|
69
|
+
|
|
70
|
+
# Start a case instance
|
|
71
|
+
case_instance = client.case_instances.start_by_key('myCase',
|
|
72
|
+
variables: { customerName: 'John Doe', amount: 1000 },
|
|
73
|
+
business_key: 'ORDER-12345'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# List and complete tasks
|
|
77
|
+
tasks = client.tasks.list(caseInstanceId: case_instance['id'])
|
|
78
|
+
client.tasks.complete(tasks['data'].first['id'],
|
|
79
|
+
variables: { approved: true }
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
### Basic Configuration
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
client = Flowable::Client.new(
|
|
89
|
+
host: 'localhost', # Required: Flowable server host
|
|
90
|
+
port: 8080, # Required: Flowable server port
|
|
91
|
+
username: 'rest-admin', # Required: Username for authentication
|
|
92
|
+
password: 'test', # Required: Password for authentication
|
|
93
|
+
use_ssl: false, # Optional: Use HTTPS (default: false)
|
|
94
|
+
base_path: '/flowable-rest' # Optional: Base path (default: '/flowable-rest')
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Environment Variables
|
|
99
|
+
|
|
100
|
+
For integration tests and CLI, you can use environment variables:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export FLOWABLE_HOST=localhost
|
|
104
|
+
export FLOWABLE_PORT=8080
|
|
105
|
+
export FLOWABLE_USER=rest-admin
|
|
106
|
+
export FLOWABLE_PASSWORD=test
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Running Flowable
|
|
110
|
+
|
|
111
|
+
Using Docker:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker run -p 8080:8080 flowable/flowable-rest:7.1.0
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Using Docker Compose (recommended for development):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker-compose up -d
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## CMMN API
|
|
126
|
+
|
|
127
|
+
### Deployments
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# List all deployments
|
|
131
|
+
deployments = client.deployments.list
|
|
132
|
+
deployments = client.deployments.list(tenantId: 'acme', sort: 'deployTime', order: 'desc')
|
|
133
|
+
|
|
134
|
+
# Deploy a CMMN file
|
|
135
|
+
deployment = client.deployments.create('/path/to/case.cmmn')
|
|
136
|
+
deployment = client.deployments.create('/path/to/case.cmmn', tenant_id: 'acme')
|
|
137
|
+
|
|
138
|
+
# Get deployment details
|
|
139
|
+
deployment = client.deployments.get('deployment-id')
|
|
140
|
+
|
|
141
|
+
# List resources in deployment
|
|
142
|
+
resources = client.deployments.resources('deployment-id')
|
|
143
|
+
|
|
144
|
+
# Get resource content (XML)
|
|
145
|
+
xml_content = client.deployments.resource_data('deployment-id', 'case.cmmn')
|
|
146
|
+
|
|
147
|
+
# Delete deployment
|
|
148
|
+
client.deployments.delete('deployment-id')
|
|
149
|
+
client.deployments.delete('deployment-id', cascade: true) # Also delete instances
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Case Definitions
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# List case definitions
|
|
156
|
+
definitions = client.case_definitions.list
|
|
157
|
+
definitions = client.case_definitions.list(
|
|
158
|
+
key: 'myCase',
|
|
159
|
+
latest: true,
|
|
160
|
+
tenantId: 'acme'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Get by ID
|
|
164
|
+
definition = client.case_definitions.get('definition-id')
|
|
165
|
+
|
|
166
|
+
# Get latest version by key
|
|
167
|
+
definition = client.case_definitions.get_by_key('myCase')
|
|
168
|
+
definition = client.case_definitions.get_by_key('myCase', tenant_id: 'acme')
|
|
169
|
+
|
|
170
|
+
# Get CMMN model (JSON representation)
|
|
171
|
+
model = client.case_definitions.model('definition-id')
|
|
172
|
+
|
|
173
|
+
# Get resource content (XML)
|
|
174
|
+
xml = client.case_definitions.resource_content('definition-id')
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Case Instances
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# List case instances
|
|
181
|
+
instances = client.case_instances.list
|
|
182
|
+
instances = client.case_instances.list(
|
|
183
|
+
caseDefinitionKey: 'myCase',
|
|
184
|
+
businessKey: 'ORDER-12345',
|
|
185
|
+
includeCaseVariables: true
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Start case instance by definition key
|
|
189
|
+
case_instance = client.case_instances.start_by_key('myCase',
|
|
190
|
+
variables: {
|
|
191
|
+
customerName: 'John Doe',
|
|
192
|
+
amount: 1000,
|
|
193
|
+
approved: false
|
|
194
|
+
},
|
|
195
|
+
business_key: 'ORDER-12345',
|
|
196
|
+
tenant_id: 'acme',
|
|
197
|
+
outcome: 'startOutcome'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Start by definition ID
|
|
201
|
+
case_instance = client.case_instances.start_by_id('definition-id',
|
|
202
|
+
variables: { foo: 'bar' }
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Get case instance details
|
|
206
|
+
instance = client.case_instances.get('instance-id')
|
|
207
|
+
|
|
208
|
+
# Get stage overview
|
|
209
|
+
stages = client.case_instances.stage_overview('instance-id')
|
|
210
|
+
stages.each do |stage|
|
|
211
|
+
status = stage['current'] ? 'ACTIVE' : (stage['ended'] ? 'COMPLETED' : 'AVAILABLE')
|
|
212
|
+
puts "#{stage['name']}: #{status}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Terminate case instance
|
|
216
|
+
client.case_instances.terminate('instance-id')
|
|
217
|
+
|
|
218
|
+
# Delete case instance
|
|
219
|
+
client.case_instances.delete('instance-id')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Case Instance Variables
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# Get all variables
|
|
226
|
+
variables = client.case_instances.variables('instance-id')
|
|
227
|
+
|
|
228
|
+
# Get single variable
|
|
229
|
+
variable = client.case_instances.variable('instance-id', 'customerName')
|
|
230
|
+
|
|
231
|
+
# Set/update multiple variables
|
|
232
|
+
client.case_instances.set_variables('instance-id', {
|
|
233
|
+
status: 'processing',
|
|
234
|
+
reviewedBy: 'kermit',
|
|
235
|
+
reviewDate: Time.now.iso8601
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
# Create variables (fails if already exist)
|
|
239
|
+
client.case_instances.create_variables('instance-id', {
|
|
240
|
+
newVariable: 'value'
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# Update single variable
|
|
244
|
+
client.case_instances.update_variable('instance-id', 'status', 'completed')
|
|
245
|
+
|
|
246
|
+
# Delete variable
|
|
247
|
+
client.case_instances.delete_variable('instance-id', 'temporaryVar')
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Tasks
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# List tasks
|
|
254
|
+
tasks = client.tasks.list
|
|
255
|
+
tasks = client.tasks.list(
|
|
256
|
+
caseInstanceId: 'instance-id',
|
|
257
|
+
assignee: 'kermit',
|
|
258
|
+
active: true
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# List claimable tasks
|
|
262
|
+
claimable = client.tasks.list(candidateUser: 'kermit')
|
|
263
|
+
claimable = client.tasks.list(candidateGroup: 'managers')
|
|
264
|
+
|
|
265
|
+
# Get task details
|
|
266
|
+
task = client.tasks.get('task-id')
|
|
267
|
+
|
|
268
|
+
# Claim task
|
|
269
|
+
client.tasks.claim('task-id', 'kermit')
|
|
270
|
+
|
|
271
|
+
# Unclaim task
|
|
272
|
+
client.tasks.unclaim('task-id')
|
|
273
|
+
|
|
274
|
+
# Complete task
|
|
275
|
+
client.tasks.complete('task-id')
|
|
276
|
+
client.tasks.complete('task-id',
|
|
277
|
+
variables: { decision: 'approved', comment: 'Looks good!' },
|
|
278
|
+
outcome: 'approve'
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Update task properties
|
|
282
|
+
client.tasks.update('task-id',
|
|
283
|
+
assignee: 'gonzo',
|
|
284
|
+
priority: 80,
|
|
285
|
+
dueDate: (Time.now + 86400).iso8601,
|
|
286
|
+
name: 'Updated Task Name',
|
|
287
|
+
description: 'New description'
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Delegate task
|
|
291
|
+
client.tasks.delegate('task-id', 'fozzie')
|
|
292
|
+
|
|
293
|
+
# Resolve delegated task
|
|
294
|
+
client.tasks.resolve('task-id')
|
|
295
|
+
|
|
296
|
+
# Delete task
|
|
297
|
+
client.tasks.delete('task-id')
|
|
298
|
+
client.tasks.delete('task-id', delete_reason: 'No longer needed')
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Task Variables
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# Get task variables
|
|
305
|
+
variables = client.tasks.variables('task-id')
|
|
306
|
+
variables = client.tasks.variables('task-id', scope: 'local') # local or global
|
|
307
|
+
|
|
308
|
+
# Create task variables
|
|
309
|
+
client.tasks.create_variables('task-id', { note: 'Important!' }, scope: 'local')
|
|
310
|
+
|
|
311
|
+
# Update variable
|
|
312
|
+
client.tasks.update_variable('task-id', 'note', 'Very important!', scope: 'local')
|
|
313
|
+
|
|
314
|
+
# Set multiple variables (create or update)
|
|
315
|
+
client.tasks.set_variables('task-id', { var1: 'a', var2: 'b' }, scope: 'local')
|
|
316
|
+
|
|
317
|
+
# Delete variable
|
|
318
|
+
client.tasks.delete_variable('task-id', 'note', scope: 'local')
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### Task Identity Links
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
# Get identity links
|
|
325
|
+
links = client.tasks.identity_links('task-id')
|
|
326
|
+
|
|
327
|
+
# Add candidate user
|
|
328
|
+
client.tasks.add_identity_link('task-id', user: 'kermit', type: 'candidate')
|
|
329
|
+
|
|
330
|
+
# Add candidate group
|
|
331
|
+
client.tasks.add_identity_link('task-id', group: 'managers', type: 'candidate')
|
|
332
|
+
|
|
333
|
+
# Delete identity link
|
|
334
|
+
client.tasks.delete_identity_link('task-id', user: 'kermit', type: 'candidate')
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Plan Item Instances
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
# List plan items for a case
|
|
341
|
+
items = client.plan_item_instances.list(caseInstanceId: 'instance-id')
|
|
342
|
+
items = client.plan_item_instances.list(
|
|
343
|
+
caseInstanceId: 'instance-id',
|
|
344
|
+
planItemDefinitionType: 'humantask',
|
|
345
|
+
state: 'active'
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Get specific plan item
|
|
349
|
+
item = client.plan_item_instances.get('plan-item-id')
|
|
350
|
+
|
|
351
|
+
# Helper methods
|
|
352
|
+
active = client.plan_item_instances.active_for_case('instance-id')
|
|
353
|
+
stages = client.plan_item_instances.stages_for_case('instance-id')
|
|
354
|
+
tasks = client.plan_item_instances.human_tasks_for_case('instance-id')
|
|
355
|
+
milestones = client.plan_item_instances.milestones_for_case('instance-id')
|
|
356
|
+
|
|
357
|
+
# Trigger actions
|
|
358
|
+
client.plan_item_instances.trigger('plan-item-id') # Trigger user event listener
|
|
359
|
+
client.plan_item_instances.enable('plan-item-id') # Enable manual activation item
|
|
360
|
+
client.plan_item_instances.disable('plan-item-id') # Disable enabled item
|
|
361
|
+
client.plan_item_instances.start('plan-item-id') # Start enabled item
|
|
362
|
+
client.plan_item_instances.terminate('plan-item-id') # Terminate active item
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### History
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Historic case instances
|
|
369
|
+
historic = client.history.case_instances
|
|
370
|
+
historic = client.history.case_instances(
|
|
371
|
+
finished: true,
|
|
372
|
+
caseDefinitionKey: 'myCase',
|
|
373
|
+
involvedUser: 'kermit'
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Get specific historic case instance
|
|
377
|
+
instance = client.history.case_instance('instance-id')
|
|
378
|
+
|
|
379
|
+
# Delete historic case instance
|
|
380
|
+
client.history.delete_case_instance('instance-id')
|
|
381
|
+
|
|
382
|
+
# Historic tasks
|
|
383
|
+
tasks = client.history.task_instances(caseInstanceId: 'instance-id')
|
|
384
|
+
tasks = client.history.task_instances(
|
|
385
|
+
finished: true,
|
|
386
|
+
taskAssignee: 'kermit'
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Historic milestones
|
|
390
|
+
milestones = client.history.milestones(caseInstanceId: 'instance-id')
|
|
391
|
+
|
|
392
|
+
# Historic plan item instances
|
|
393
|
+
items = client.history.plan_item_instances(caseInstanceId: 'instance-id')
|
|
394
|
+
|
|
395
|
+
# Historic variables
|
|
396
|
+
variables = client.history.variable_instances(caseInstanceId: 'instance-id')
|
|
397
|
+
|
|
398
|
+
# Query with filters
|
|
399
|
+
results = client.history.query_case_instances(
|
|
400
|
+
caseDefinitionKey: 'myCase',
|
|
401
|
+
finished: true
|
|
402
|
+
)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## BPMN API
|
|
408
|
+
|
|
409
|
+
The client also provides full support for Flowable's BPMN engine.
|
|
410
|
+
|
|
411
|
+
### BPMN Deployments
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
# List deployments
|
|
415
|
+
deployments = client.bpmn_deployments.list
|
|
416
|
+
|
|
417
|
+
# Deploy BPMN file
|
|
418
|
+
deployment = client.bpmn_deployments.create('/path/to/process.bpmn20.xml')
|
|
419
|
+
|
|
420
|
+
# Get deployment
|
|
421
|
+
deployment = client.bpmn_deployments.get('deployment-id')
|
|
422
|
+
|
|
423
|
+
# Delete deployment
|
|
424
|
+
client.bpmn_deployments.delete('deployment-id', cascade: true)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Process Definitions
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
# List process definitions
|
|
431
|
+
definitions = client.process_definitions.list
|
|
432
|
+
definitions = client.process_definitions.list(key: 'myProcess', latest: true)
|
|
433
|
+
|
|
434
|
+
# Get by ID
|
|
435
|
+
definition = client.process_definitions.get('definition-id')
|
|
436
|
+
|
|
437
|
+
# Get latest by key
|
|
438
|
+
definition = client.process_definitions.get_by_key('myProcess')
|
|
439
|
+
|
|
440
|
+
# Get BPMN model
|
|
441
|
+
model = client.process_definitions.model('definition-id')
|
|
442
|
+
|
|
443
|
+
# Get resource content
|
|
444
|
+
xml = client.process_definitions.resource_content('definition-id')
|
|
445
|
+
|
|
446
|
+
# Suspend/Activate
|
|
447
|
+
client.process_definitions.suspend('definition-id')
|
|
448
|
+
client.process_definitions.activate('definition-id')
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Process Instances
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
# List process instances
|
|
455
|
+
instances = client.process_instances.list
|
|
456
|
+
instances = client.process_instances.list(
|
|
457
|
+
processDefinitionKey: 'myProcess',
|
|
458
|
+
includeProcessVariables: true
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Start process instance
|
|
462
|
+
instance = client.process_instances.start_by_key('myProcess',
|
|
463
|
+
variables: { orderId: 'ORD-123', amount: 500 },
|
|
464
|
+
business_key: 'ORDER-123'
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Get process instance
|
|
468
|
+
instance = client.process_instances.get('instance-id')
|
|
469
|
+
|
|
470
|
+
# Get diagram (PNG)
|
|
471
|
+
diagram = client.process_instances.diagram('instance-id')
|
|
472
|
+
|
|
473
|
+
# Suspend/Activate
|
|
474
|
+
client.process_instances.suspend('instance-id')
|
|
475
|
+
client.process_instances.activate('instance-id')
|
|
476
|
+
|
|
477
|
+
# Delete
|
|
478
|
+
client.process_instances.delete('instance-id')
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### Process Instance Variables
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
# Get variables
|
|
485
|
+
variables = client.process_instances.variables('instance-id')
|
|
486
|
+
|
|
487
|
+
# Get single variable
|
|
488
|
+
variable = client.process_instances.variable('instance-id', 'orderId')
|
|
489
|
+
|
|
490
|
+
# Set variables
|
|
491
|
+
client.process_instances.set_variables('instance-id', {
|
|
492
|
+
status: 'processing',
|
|
493
|
+
updatedAt: Time.now.iso8601
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
# Update single variable
|
|
497
|
+
client.process_instances.update_variable('instance-id', 'status', 'completed')
|
|
498
|
+
|
|
499
|
+
# Delete variable
|
|
500
|
+
client.process_instances.delete_variable('instance-id', 'tempVar')
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### BPMN Tasks
|
|
504
|
+
|
|
505
|
+
BPMN tasks use the same `client.tasks` interface as CMMN. The API automatically routes requests based on the context.
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
# List all tasks (both CMMN and BPMN)
|
|
509
|
+
tasks = client.tasks.list
|
|
510
|
+
|
|
511
|
+
# Filter by process instance
|
|
512
|
+
tasks = client.tasks.list(processInstanceId: 'process-instance-id')
|
|
513
|
+
|
|
514
|
+
# All task operations work the same way
|
|
515
|
+
client.tasks.claim('task-id', 'kermit')
|
|
516
|
+
client.tasks.complete('task-id', variables: { approved: true })
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Executions
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
# List executions
|
|
523
|
+
executions = client.executions.list(processInstanceId: 'instance-id')
|
|
524
|
+
|
|
525
|
+
# Get execution
|
|
526
|
+
execution = client.executions.get('execution-id')
|
|
527
|
+
|
|
528
|
+
# Get active activities
|
|
529
|
+
activities = client.executions.activities('execution-id')
|
|
530
|
+
|
|
531
|
+
# Signal execution
|
|
532
|
+
client.executions.signal('execution-id', variables: { signalData: 'value' })
|
|
533
|
+
|
|
534
|
+
# Trigger execution
|
|
535
|
+
client.executions.trigger('execution-id')
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### BPMN History
|
|
539
|
+
|
|
540
|
+
```ruby
|
|
541
|
+
# Historic process instances
|
|
542
|
+
historic = client.bpmn_history.process_instances
|
|
543
|
+
historic = client.bpmn_history.process_instances(
|
|
544
|
+
finished: true,
|
|
545
|
+
processDefinitionKey: 'myProcess'
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Historic activities
|
|
549
|
+
activities = client.bpmn_history.activity_instances(processInstanceId: 'instance-id')
|
|
550
|
+
|
|
551
|
+
# Historic tasks
|
|
552
|
+
tasks = client.bpmn_history.task_instances(processInstanceId: 'instance-id')
|
|
553
|
+
|
|
554
|
+
# Historic variables
|
|
555
|
+
variables = client.bpmn_history.variable_instances(processInstanceId: 'instance-id')
|
|
556
|
+
|
|
557
|
+
# Query process instances
|
|
558
|
+
results = client.bpmn_history.query_process_instances({
|
|
559
|
+
processDefinitionKey: 'myProcess',
|
|
560
|
+
finished: true,
|
|
561
|
+
variables: [
|
|
562
|
+
{ name: 'amount', value: 1000, operation: 'greaterThan', type: 'long' }
|
|
563
|
+
]
|
|
564
|
+
})
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Working with Variables
|
|
570
|
+
|
|
571
|
+
### Automatic Type Inference
|
|
572
|
+
|
|
573
|
+
The client automatically infers variable types:
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
client.case_instances.set_variables('instance-id', {
|
|
577
|
+
stringVar: 'hello', # string
|
|
578
|
+
intVar: 42, # integer
|
|
579
|
+
floatVar: 3.14, # double
|
|
580
|
+
boolVar: true, # boolean
|
|
581
|
+
dateVar: Time.now, # date (ISO-8601)
|
|
582
|
+
arrayVar: [1, 2, 3], # json
|
|
583
|
+
hashVar: { a: 1, b: 2 } # json
|
|
584
|
+
})
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Explicit Type Specification
|
|
588
|
+
|
|
589
|
+
For query operations, specify types explicitly:
|
|
590
|
+
|
|
591
|
+
```ruby
|
|
592
|
+
results = client.history.query_case_instances({
|
|
593
|
+
variables: [
|
|
594
|
+
{ name: 'amount', value: 1000, operation: 'greaterThan', type: 'long' },
|
|
595
|
+
{ name: 'status', value: 'completed', operation: 'equals', type: 'string' },
|
|
596
|
+
{ name: 'approved', value: true, operation: 'equals', type: 'boolean' }
|
|
597
|
+
]
|
|
598
|
+
})
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Supported Operations
|
|
602
|
+
|
|
603
|
+
| Operation | Description |
|
|
604
|
+
|-----------|-------------|
|
|
605
|
+
| `equals` | Exact match |
|
|
606
|
+
| `notEquals` | Not equal |
|
|
607
|
+
| `greaterThan` | Greater than (numeric) |
|
|
608
|
+
| `greaterThanOrEquals` | Greater than or equal |
|
|
609
|
+
| `lessThan` | Less than (numeric) |
|
|
610
|
+
| `lessThanOrEquals` | Less than or equal |
|
|
611
|
+
| `like` | Pattern match (use `%` as wildcard) |
|
|
612
|
+
| `likeIgnoreCase` | Case-insensitive pattern match |
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Error Handling
|
|
617
|
+
|
|
618
|
+
The client raises specific exceptions for different error conditions:
|
|
619
|
+
|
|
620
|
+
```ruby
|
|
621
|
+
begin
|
|
622
|
+
client.case_instances.get('non-existent-id')
|
|
623
|
+
rescue Flowable::NotFoundError => e
|
|
624
|
+
# 404 - Resource not found
|
|
625
|
+
puts "Not found: #{e.message}"
|
|
626
|
+
rescue Flowable::UnauthorizedError => e
|
|
627
|
+
# 401 - Authentication failed
|
|
628
|
+
puts "Auth failed: #{e.message}"
|
|
629
|
+
rescue Flowable::ForbiddenError => e
|
|
630
|
+
# 403 - Access denied
|
|
631
|
+
puts "Forbidden: #{e.message}"
|
|
632
|
+
rescue Flowable::BadRequestError => e
|
|
633
|
+
# 400 - Invalid request
|
|
634
|
+
puts "Bad request: #{e.message}"
|
|
635
|
+
rescue Flowable::ConflictError => e
|
|
636
|
+
# 409 - Conflict (e.g., duplicate resource)
|
|
637
|
+
puts "Conflict: #{e.message}"
|
|
638
|
+
rescue Flowable::Error => e
|
|
639
|
+
# Other errors
|
|
640
|
+
puts "Error: #{e.message}"
|
|
641
|
+
end
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Exception Hierarchy
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
Flowable::Error
|
|
648
|
+
├── Flowable::BadRequestError (400)
|
|
649
|
+
├── Flowable::UnauthorizedError (401)
|
|
650
|
+
├── Flowable::ForbiddenError (403)
|
|
651
|
+
├── Flowable::NotFoundError (404)
|
|
652
|
+
└── Flowable::ConflictError (409)
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Pagination
|
|
658
|
+
|
|
659
|
+
### Basic Pagination
|
|
660
|
+
|
|
661
|
+
```ruby
|
|
662
|
+
# First page (default size: 10)
|
|
663
|
+
page1 = client.tasks.list(start: 0, size: 10)
|
|
664
|
+
# => { 'data' => [...], 'total' => 100, 'start' => 0, 'size' => 10 }
|
|
665
|
+
|
|
666
|
+
# Second page
|
|
667
|
+
page2 = client.tasks.list(start: 10, size: 10)
|
|
668
|
+
|
|
669
|
+
# With sorting
|
|
670
|
+
sorted = client.tasks.list(
|
|
671
|
+
sort: 'createTime',
|
|
672
|
+
order: 'desc',
|
|
673
|
+
size: 20
|
|
674
|
+
)
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Iterating All Results
|
|
678
|
+
|
|
679
|
+
```ruby
|
|
680
|
+
def each_task(client)
|
|
681
|
+
start = 0
|
|
682
|
+
size = 100
|
|
683
|
+
|
|
684
|
+
loop do
|
|
685
|
+
result = client.tasks.list(start: start, size: size)
|
|
686
|
+
result['data'].each { |task| yield task }
|
|
687
|
+
|
|
688
|
+
break if start + size >= result['total']
|
|
689
|
+
start += size
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Usage
|
|
694
|
+
each_task(client) do |task|
|
|
695
|
+
puts "#{task['id']}: #{task['name']}"
|
|
696
|
+
end
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Sorting Options
|
|
700
|
+
|
|
701
|
+
| Resource | Available Sort Fields |
|
|
702
|
+
|----------|----------------------|
|
|
703
|
+
| Tasks | `id`, `name`, `priority`, `assignee`, `createTime`, `dueDate` |
|
|
704
|
+
| Case Instances | `id`, `caseDefinitionId`, `startTime`, `businessKey` |
|
|
705
|
+
| Process Instances | `id`, `processDefinitionId`, `startTime`, `businessKey` |
|
|
706
|
+
| Deployments | `id`, `name`, `deployTime`, `tenantId` |
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
## CLI Tool
|
|
711
|
+
|
|
712
|
+
The gem includes a command-line interface for common operations:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
# Set connection details
|
|
716
|
+
export FLOWABLE_HOST=localhost
|
|
717
|
+
export FLOWABLE_PORT=8080
|
|
718
|
+
export FLOWABLE_USER=rest-admin
|
|
719
|
+
export FLOWABLE_PASSWORD=test
|
|
720
|
+
|
|
721
|
+
# List deployments
|
|
722
|
+
bin/flowable deployments list
|
|
723
|
+
|
|
724
|
+
# Deploy a case
|
|
725
|
+
bin/flowable deployments create my-case.cmmn
|
|
726
|
+
|
|
727
|
+
# List case definitions
|
|
728
|
+
bin/flowable case-definitions list
|
|
729
|
+
|
|
730
|
+
# Start a case
|
|
731
|
+
bin/flowable case-instances start --key myCase --variables '{"amount":1000}'
|
|
732
|
+
|
|
733
|
+
# List tasks
|
|
734
|
+
bin/flowable tasks list
|
|
735
|
+
|
|
736
|
+
# Complete a task
|
|
737
|
+
bin/flowable tasks complete TASK_ID --variables '{"approved":true}'
|
|
738
|
+
|
|
739
|
+
# Get help
|
|
740
|
+
bin/flowable --help
|
|
741
|
+
bin/flowable tasks --help
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### CLI Commands
|
|
745
|
+
|
|
746
|
+
| Command | Description |
|
|
747
|
+
|---------|-------------|
|
|
748
|
+
| `deployments list` | List all deployments |
|
|
749
|
+
| `deployments create FILE` | Deploy a CMMN/BPMN file |
|
|
750
|
+
| `deployments delete ID` | Delete a deployment |
|
|
751
|
+
| `case-definitions list` | List case definitions |
|
|
752
|
+
| `case-definitions get ID` | Get case definition details |
|
|
753
|
+
| `case-instances list` | List case instances |
|
|
754
|
+
| `case-instances start` | Start a new case instance |
|
|
755
|
+
| `case-instances get ID` | Get case instance details |
|
|
756
|
+
| `tasks list` | List tasks |
|
|
757
|
+
| `tasks get ID` | Get task details |
|
|
758
|
+
| `tasks claim ID USER` | Claim a task |
|
|
759
|
+
| `tasks complete ID` | Complete a task |
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## Workflow DSL
|
|
764
|
+
|
|
765
|
+
For complex workflows, use the DSL:
|
|
766
|
+
|
|
767
|
+
```ruby
|
|
768
|
+
require 'flowable/dsl'
|
|
769
|
+
|
|
770
|
+
workflow = Flowable::Workflow.define do
|
|
771
|
+
name 'Order Processing'
|
|
772
|
+
|
|
773
|
+
on_start do |ctx|
|
|
774
|
+
ctx[:started_at] = Time.now
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
step :validate_order do |ctx|
|
|
778
|
+
raise 'Invalid amount' if ctx.variable(:amount) <= 0
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
step :process_payment do |ctx|
|
|
782
|
+
# Process payment logic
|
|
783
|
+
ctx.set_variable(:payment_status, 'completed')
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
step :ship_order do |ctx|
|
|
787
|
+
# Shipping logic
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
on_error do |ctx, error|
|
|
791
|
+
puts "Error: #{error.message}"
|
|
792
|
+
ctx.set_variable(:error, error.message)
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
on_complete do |ctx|
|
|
796
|
+
puts "Completed in #{Time.now - ctx[:started_at]} seconds"
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Execute
|
|
801
|
+
client = Flowable::Client.new(...)
|
|
802
|
+
workflow.execute(client, 'case-instance-id')
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## Testing
|
|
808
|
+
|
|
809
|
+
### Running the Test Suite
|
|
810
|
+
|
|
811
|
+
```bash
|
|
812
|
+
# Start Flowable container
|
|
813
|
+
docker-compose up -d
|
|
814
|
+
|
|
815
|
+
# Wait for Flowable to be ready
|
|
816
|
+
./run_tests.sh --wait
|
|
817
|
+
|
|
818
|
+
# Run integration tests
|
|
819
|
+
./run_tests.sh --integration
|
|
820
|
+
|
|
821
|
+
# Run unit tests (no container needed)
|
|
822
|
+
./run_tests.sh --unit
|
|
823
|
+
|
|
824
|
+
# Run all tests
|
|
825
|
+
./run_tests.sh --all
|
|
826
|
+
|
|
827
|
+
# With automatic container management
|
|
828
|
+
./run_tests.sh --start --integration --stop
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
## Known Issues
|
|
832
|
+
|
|
833
|
+
### Date Parameter Parsing (Flowable 7.1.0)
|
|
834
|
+
|
|
835
|
+
⚠️ Flowable REST API 7.1.0 has a bug where date parameters in query strings are incorrectly parsed. Parameters like `startedAfter`, `finishedBefore`, etc. may fail with "Failed to parse date" errors.
|
|
836
|
+
|
|
837
|
+
**Workaround:** Use non-date filters or upgrade to a newer Flowable version when available.
|
|
838
|
+
|
|
839
|
+
### Model Endpoint Nesting Limit
|
|
840
|
+
|
|
841
|
+
⚠️ The `/model` endpoint for case/process definitions may return malformed JSON for complex models due to Jackson's default nesting depth limit (1000). The client handles this gracefully by skipping malformed responses.
|
|
842
|
+
|
|
843
|
+
### XML Resource Content-Type
|
|
844
|
+
|
|
845
|
+
⚠️ The `resourcedata` endpoint returns XML content with `Content-Type: application/json`. The client automatically detects and handles this by checking if the response body starts with `<?xml`.
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
849
|
+
## Contributing
|
|
850
|
+
|
|
851
|
+
1. Fork the repository
|
|
852
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
853
|
+
3. Write tests for your changes
|
|
854
|
+
4. Ensure all tests pass (`./run_tests.sh --all`)
|
|
855
|
+
5. Commit your changes (`git commit -am 'Add amazing feature'`)
|
|
856
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
857
|
+
7. Open a Pull Request
|
|
858
|
+
|
|
859
|
+
## License
|
|
860
|
+
|
|
861
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
862
|
+
|
|
863
|
+
## Acknowledgments
|
|
864
|
+
|
|
865
|
+
- [Flowable](https://www.flowable.com/) - The powerful open-source BPM/CMMN platform
|
|
866
|
+
- [Flowable REST API Documentation](https://www.flowable.com/open-source/docs/cmmn/ch14-REST)
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
<p align="center">
|
|
871
|
+
<strong>Made with ❤️ for the Ruby and Flowable communities</strong>
|
|
872
|
+
</p>
|