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.
data/README.md ADDED
@@ -0,0 +1,872 @@
1
+ # Flowable
2
+
3
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7.0-ruby.svg)](https://www.ruby-lang.org/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Flowable](https://img.shields.io/badge/Flowable-7.1.0-blue.svg)](https://www.flowable.com/)
6
+
7
+ ![Flowable logo](flowable.png)
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>