monday_ruby 1.1.0 → 1.2.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.env +1 -1
  3. data/.rubocop.yml +2 -1
  4. data/CHANGELOG.md +14 -0
  5. data/CONTRIBUTING.md +104 -0
  6. data/README.md +146 -142
  7. data/docs/.vitepress/config.mjs +255 -0
  8. data/docs/.vitepress/theme/index.js +4 -0
  9. data/docs/.vitepress/theme/style.css +43 -0
  10. data/docs/README.md +80 -0
  11. data/docs/explanation/architecture.md +507 -0
  12. data/docs/explanation/best-practices/errors.md +478 -0
  13. data/docs/explanation/best-practices/performance.md +1084 -0
  14. data/docs/explanation/best-practices/rate-limiting.md +630 -0
  15. data/docs/explanation/best-practices/testing.md +820 -0
  16. data/docs/explanation/column-values.md +857 -0
  17. data/docs/explanation/design.md +795 -0
  18. data/docs/explanation/graphql.md +356 -0
  19. data/docs/explanation/migration/v1.md +808 -0
  20. data/docs/explanation/pagination.md +447 -0
  21. data/docs/guides/advanced/batch.md +1274 -0
  22. data/docs/guides/advanced/complex-queries.md +1114 -0
  23. data/docs/guides/advanced/errors.md +818 -0
  24. data/docs/guides/advanced/pagination.md +934 -0
  25. data/docs/guides/advanced/rate-limiting.md +981 -0
  26. data/docs/guides/authentication.md +286 -0
  27. data/docs/guides/boards/create.md +386 -0
  28. data/docs/guides/boards/delete.md +405 -0
  29. data/docs/guides/boards/duplicate.md +511 -0
  30. data/docs/guides/boards/query.md +530 -0
  31. data/docs/guides/boards/update.md +453 -0
  32. data/docs/guides/columns/create.md +452 -0
  33. data/docs/guides/columns/metadata.md +492 -0
  34. data/docs/guides/columns/query.md +455 -0
  35. data/docs/guides/columns/update-multiple.md +459 -0
  36. data/docs/guides/columns/update-values.md +509 -0
  37. data/docs/guides/files/add-to-column.md +40 -0
  38. data/docs/guides/files/add-to-update.md +37 -0
  39. data/docs/guides/files/clear-column.md +33 -0
  40. data/docs/guides/first-request.md +285 -0
  41. data/docs/guides/folders/manage.md +750 -0
  42. data/docs/guides/groups/items.md +626 -0
  43. data/docs/guides/groups/manage.md +501 -0
  44. data/docs/guides/installation.md +169 -0
  45. data/docs/guides/items/create.md +493 -0
  46. data/docs/guides/items/delete.md +514 -0
  47. data/docs/guides/items/query.md +605 -0
  48. data/docs/guides/items/subitems.md +483 -0
  49. data/docs/guides/items/update.md +699 -0
  50. data/docs/guides/updates/manage.md +619 -0
  51. data/docs/guides/use-cases/dashboard.md +1421 -0
  52. data/docs/guides/use-cases/import.md +1962 -0
  53. data/docs/guides/use-cases/task-management.md +1381 -0
  54. data/docs/guides/workspaces/manage.md +502 -0
  55. data/docs/index.md +69 -0
  56. data/docs/package-lock.json +2468 -0
  57. data/docs/package.json +13 -0
  58. data/docs/reference/client.md +540 -0
  59. data/docs/reference/configuration.md +586 -0
  60. data/docs/reference/errors.md +693 -0
  61. data/docs/reference/resources/account.md +208 -0
  62. data/docs/reference/resources/activity-log.md +369 -0
  63. data/docs/reference/resources/board-view.md +359 -0
  64. data/docs/reference/resources/board.md +393 -0
  65. data/docs/reference/resources/column.md +543 -0
  66. data/docs/reference/resources/file.md +236 -0
  67. data/docs/reference/resources/folder.md +386 -0
  68. data/docs/reference/resources/group.md +507 -0
  69. data/docs/reference/resources/item.md +348 -0
  70. data/docs/reference/resources/subitem.md +267 -0
  71. data/docs/reference/resources/update.md +259 -0
  72. data/docs/reference/resources/workspace.md +213 -0
  73. data/docs/reference/response.md +560 -0
  74. data/docs/tutorial/first-integration.md +713 -0
  75. data/lib/monday/client.rb +24 -0
  76. data/lib/monday/configuration.rb +5 -0
  77. data/lib/monday/request.rb +15 -0
  78. data/lib/monday/resources/base.rb +4 -0
  79. data/lib/monday/resources/file.rb +56 -0
  80. data/lib/monday/util.rb +1 -0
  81. data/lib/monday/version.rb +1 -1
  82. metadata +87 -4
@@ -0,0 +1,981 @@
1
+ # Handle Rate Limits
2
+
3
+ Manage monday.com API rate limits and complexity budgets to prevent errors and optimize API usage.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Installed and configured](/guides/installation) monday_ruby
8
+ - [Set up authentication](/guides/authentication) with your API token
9
+ - Understanding of [basic requests](/guides/first-request)
10
+
11
+ ## Understanding Rate Limits
12
+
13
+ monday.com uses a **complexity budget system** to manage API load. Every query consumes complexity points based on the data requested.
14
+
15
+ ### Complexity Limits by Plan
16
+
17
+ **Per-minute limits** (sliding 60-second window):
18
+
19
+ | Token Type | Plan | Complexity Budget |
20
+ |------------|------|-------------------|
21
+ | Personal API Token | Free/Trial/NGO | 1,000,000 points |
22
+ | Personal API Token | Paid Plans | 10,000,000 points |
23
+ | App Token | All Plans | 5,000,000 (read) + 5,000,000 (write) |
24
+ | API Playground | Free/Trial | 1,000,000 points |
25
+ | API Playground | Paid Plans | 5,000,000 points |
26
+
27
+ **Per-query limit**: 5,000,000 complexity points maximum
28
+
29
+ ### Daily Call Limits
30
+
31
+ Daily limits reset at **midnight UTC**:
32
+
33
+ | Plan | Daily Calls |
34
+ |------|-------------|
35
+ | Free/Trial | 200 |
36
+ | Standard/Basic | 1,000 |
37
+ | Pro | 10,000 (soft limit) |
38
+ | Enterprise | 25,000 (soft limit) |
39
+
40
+ **Note**: Rate-limited requests count as 0.1 calls; high-complexity queries count as 1+ calls.
41
+
42
+ ### Common Query Complexity Costs
43
+
44
+ Approximate complexity points per operation:
45
+
46
+ - **Simple query** (boards list): ~500-1,000 points
47
+ - **Create item**: ~10,000 points
48
+ - **Nested query** (boards with items): ~5,000-50,000 points
49
+ - **Bulk operations**: Varies based on batch size
50
+
51
+ ::: tip <span style="display: inline-flex; align-items: center; gap: 6px;"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>Estimate Before Creating</span>
52
+ When creating items in bulk, avoid creating more than 10-20 items in rapid succession to prevent hitting the complexity limit.
53
+ :::
54
+
55
+ ## Monitor Complexity Usage
56
+
57
+ Check your complexity budget using the complexity query:
58
+
59
+ ```ruby
60
+ require "monday_ruby"
61
+
62
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
63
+
64
+ # Query with complexity field to monitor usage
65
+ query = <<~GRAPHQL
66
+ query {
67
+ complexity {
68
+ before
69
+ after
70
+ query
71
+ reset_in_x_seconds
72
+ }
73
+ boards(limit: 1) {
74
+ id
75
+ name
76
+ }
77
+ }
78
+ GRAPHQL
79
+
80
+ response = client.make_request(query)
81
+
82
+ if response.success?
83
+ complexity = response.body.dig("data", "complexity")
84
+
85
+ puts "Complexity Budget Status:"
86
+ puts " Before query: #{complexity['before']} points"
87
+ puts " Query cost: #{complexity['query']} points"
88
+ puts " After query: #{complexity['after']} points"
89
+ puts " Resets in: #{complexity['reset_in_x_seconds']} seconds"
90
+ end
91
+ ```
92
+
93
+ **Example output:**
94
+ ```
95
+ Complexity Budget Status:
96
+ Before query: 10000000 points
97
+ Query cost: 883 points
98
+ After query: 9999117 points
99
+ Resets in: 45 seconds
100
+ ```
101
+
102
+ ## Handle Rate Limit Errors
103
+
104
+ The gem raises `Monday::RateLimitError` when rate limits are exceeded:
105
+
106
+ ```ruby
107
+ require "monday_ruby"
108
+
109
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
110
+
111
+ begin
112
+ response = client.board.query(
113
+ args: { limit: 100 },
114
+ select: ["id", "name", { items: ["id", "name", "column_values"] }]
115
+ )
116
+
117
+ boards = response.body.dig("data", "boards")
118
+ puts "Successfully retrieved #{boards.length} boards"
119
+
120
+ rescue Monday::RateLimitError => e
121
+ puts "Rate limit exceeded!"
122
+ puts "Error: #{e.message}"
123
+ puts "Error code: #{e.code}"
124
+
125
+ # Check for retry timing in error data
126
+ retry_seconds = e.error_data["retry_in_seconds"]
127
+
128
+ if retry_seconds
129
+ puts "Retry after #{retry_seconds} seconds"
130
+ else
131
+ puts "Wait 60 seconds before retrying"
132
+ end
133
+ end
134
+ ```
135
+
136
+ ### Error Response Structure
137
+
138
+ Rate limit errors include helpful metadata:
139
+
140
+ ```ruby
141
+ begin
142
+ # Expensive query that exceeds limit
143
+ response = client.item.create(
144
+ args: { board_id: 1234567890, item_name: "Test" }
145
+ )
146
+ rescue Monday::RateLimitError => e
147
+ # Access error details
148
+ puts "Message: #{e.message}"
149
+ # => "ComplexityException" or "Complexity budget exhausted"
150
+
151
+ puts "Code: #{e.code}"
152
+ # => 429
153
+
154
+ puts "Error data: #{e.error_data}"
155
+ # => {"retry_in_seconds"=>30} (if available)
156
+ end
157
+ ```
158
+
159
+ ## Retry with Exponential Backoff
160
+
161
+ Implement automatic retry logic with progressive delays:
162
+
163
+ ```ruby
164
+ require "monday_ruby"
165
+
166
+ def make_request_with_retry(client, max_retries: 3)
167
+ retries = 0
168
+ base_delay = 2 # seconds
169
+
170
+ begin
171
+ response = yield(client)
172
+ return response
173
+
174
+ rescue Monday::RateLimitError => e
175
+ retries += 1
176
+
177
+ if retries <= max_retries
178
+ # Calculate exponential backoff delay
179
+ delay = base_delay * (2 ** (retries - 1))
180
+
181
+ # Use retry_in_seconds from API if available
182
+ if e.error_data["retry_in_seconds"]
183
+ delay = e.error_data["retry_in_seconds"]
184
+ end
185
+
186
+ puts "Rate limit hit. Retry #{retries}/#{max_retries} in #{delay}s..."
187
+ sleep(delay)
188
+ retry
189
+ else
190
+ puts "Max retries exceeded. Giving up."
191
+ raise
192
+ end
193
+ end
194
+ end
195
+
196
+ # Usage
197
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
198
+
199
+ response = make_request_with_retry(client, max_retries: 3) do |c|
200
+ c.board.query(
201
+ args: { limit: 50 },
202
+ select: ["id", "name", { items: ["id", "name"] }]
203
+ )
204
+ end
205
+
206
+ if response.success?
207
+ boards = response.body.dig("data", "boards")
208
+ puts "Retrieved #{boards.length} boards"
209
+ end
210
+ ```
211
+
212
+ ### Advanced Retry with Jitter
213
+
214
+ Add random jitter to prevent thundering herd:
215
+
216
+ ```ruby
217
+ require "monday_ruby"
218
+
219
+ def exponential_backoff_with_jitter(retries, base_delay: 2, max_delay: 60)
220
+ # Calculate exponential backoff
221
+ delay = [base_delay * (2 ** (retries - 1)), max_delay].min
222
+
223
+ # Add random jitter (±25%)
224
+ jitter = delay * 0.25 * (rand - 0.5) * 2
225
+ delay + jitter
226
+ end
227
+
228
+ def robust_api_call(client, max_retries: 5)
229
+ retries = 0
230
+
231
+ begin
232
+ yield(client)
233
+
234
+ rescue Monday::RateLimitError => e
235
+ retries += 1
236
+
237
+ if retries <= max_retries
238
+ delay = exponential_backoff_with_jitter(retries)
239
+
240
+ # Prefer API-provided retry timing
241
+ if e.error_data["retry_in_seconds"]
242
+ delay = e.error_data["retry_in_seconds"]
243
+ end
244
+
245
+ puts "[Retry #{retries}/#{max_retries}] Waiting #{delay.round(2)}s..."
246
+ sleep(delay)
247
+ retry
248
+ else
249
+ raise
250
+ end
251
+ end
252
+ end
253
+
254
+ # Usage
255
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
256
+
257
+ begin
258
+ response = robust_api_call(client, max_retries: 5) do |c|
259
+ c.item.create(
260
+ args: {
261
+ board_id: 1234567890,
262
+ item_name: "High Priority Task"
263
+ },
264
+ select: ["id", "name"]
265
+ )
266
+ end
267
+
268
+ item = response.body.dig("data", "create_item")
269
+ puts "Created item: #{item['name']} (ID: #{item['id']})"
270
+
271
+ rescue Monday::RateLimitError
272
+ puts "Failed after all retries due to rate limiting"
273
+ end
274
+ ```
275
+
276
+ ## Rate Limiting Strategies
277
+
278
+ ### Add Delays Between Requests
279
+
280
+ Prevent rate limit errors by spacing out API calls:
281
+
282
+ ```ruby
283
+ require "monday_ruby"
284
+
285
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
286
+
287
+ board_ids = [1234567890, 2345678901, 3456789012, 4567890123]
288
+ boards_data = []
289
+
290
+ board_ids.each_with_index do |board_id, index|
291
+ # Add delay after each request (except the first)
292
+ sleep(0.5) if index > 0
293
+
294
+ response = client.board.query(
295
+ args: { ids: [board_id] },
296
+ select: ["id", "name", { items: ["id", "name"] }]
297
+ )
298
+
299
+ if response.success?
300
+ board = response.body.dig("data", "boards", 0)
301
+ boards_data << board
302
+ puts "Fetched board: #{board['name']}"
303
+ end
304
+ end
305
+
306
+ puts "Total boards fetched: #{boards_data.length}"
307
+ ```
308
+
309
+ ### Batch Operations Efficiently
310
+
311
+ Group operations to reduce API calls:
312
+
313
+ ```ruby
314
+ require "monday_ruby"
315
+
316
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
317
+
318
+ # Instead of querying boards one by one:
319
+ # ❌ Multiple requests (inefficient)
320
+ board_ids = [1234567890, 2345678901, 3456789012]
321
+ boards = board_ids.map do |id|
322
+ sleep(0.5) # Still need delays to avoid rate limits
323
+ response = client.board.query(args: { ids: [id] })
324
+ response.body.dig("data", "boards", 0)
325
+ end
326
+
327
+ # ✅ Single request (efficient)
328
+ response = client.board.query(
329
+ args: { ids: board_ids },
330
+ select: ["id", "name", "description"]
331
+ )
332
+
333
+ boards = response.body.dig("data", "boards")
334
+ puts "Fetched #{boards.length} boards in one request"
335
+ ```
336
+
337
+ ### Use Pagination to Control Load
338
+
339
+ Request data in smaller chunks:
340
+
341
+ ```ruby
342
+ require "monday_ruby"
343
+
344
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
345
+
346
+ def fetch_items_paginated(client, board_id, page_size: 25)
347
+ all_items = []
348
+ page = 1
349
+
350
+ loop do
351
+ response = client.board.query(
352
+ args: { ids: [board_id] },
353
+ select: [
354
+ "id",
355
+ "name",
356
+ {
357
+ items_page: {
358
+ args: { limit: page_size, query_params: { page: page } },
359
+ select: ["cursor", { items: ["id", "name"] }]
360
+ }
361
+ }
362
+ ]
363
+ )
364
+
365
+ board = response.body.dig("data", "boards", 0)
366
+ items_page = board.dig("items_page")
367
+ items = items_page["items"]
368
+
369
+ all_items.concat(items)
370
+ puts "Fetched page #{page}: #{items.length} items"
371
+
372
+ # Stop if we got fewer items than page size
373
+ break if items.length < page_size
374
+
375
+ page += 1
376
+
377
+ # Add delay between pages
378
+ sleep(0.5)
379
+ end
380
+
381
+ all_items
382
+ end
383
+
384
+ # Usage
385
+ items = fetch_items_paginated(client, 1234567890, page_size: 50)
386
+ puts "Total items fetched: #{items.length}"
387
+ ```
388
+
389
+ ### Reduce Query Complexity
390
+
391
+ Request only essential fields:
392
+
393
+ ```ruby
394
+ require "monday_ruby"
395
+
396
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
397
+
398
+ # ❌ High complexity (requests everything)
399
+ response = client.board.query(
400
+ args: { ids: [1234567890] },
401
+ select: [
402
+ "id", "name", "description", "state", "board_folder_id",
403
+ {
404
+ items: [
405
+ "id", "name", "state", "created_at", "updated_at",
406
+ { column_values: ["id", "text", "value", "type"] },
407
+ { updates: ["id", "body", "created_at"] }
408
+ ]
409
+ },
410
+ { groups: ["id", "title", "color"] },
411
+ { columns: ["id", "title", "type", "settings_str"] }
412
+ ]
413
+ )
414
+
415
+ # ✅ Low complexity (requests only what's needed)
416
+ response = client.board.query(
417
+ args: { ids: [1234567890] },
418
+ select: [
419
+ "id",
420
+ "name",
421
+ { items: ["id", "name"] }
422
+ ]
423
+ )
424
+
425
+ if response.success?
426
+ board = response.body.dig("data", "boards", 0)
427
+ puts "Board: #{board['name']}, Items: #{board['items'].length}"
428
+ end
429
+ ```
430
+
431
+ ## Production-Ready Rate Limiter
432
+
433
+ Create a reusable rate limiter class:
434
+
435
+ ```ruby
436
+ require "monday_ruby"
437
+
438
+ class MondayRateLimiter
439
+ def initialize(client, requests_per_minute: 60)
440
+ @client = client
441
+ @requests_per_minute = requests_per_minute
442
+ @min_delay = 60.0 / requests_per_minute
443
+ @last_request_time = nil
444
+ end
445
+
446
+ def execute(max_retries: 3, &block)
447
+ enforce_rate_limit
448
+
449
+ retries = 0
450
+
451
+ begin
452
+ response = block.call(@client)
453
+ @last_request_time = Time.now
454
+ response
455
+
456
+ rescue Monday::RateLimitError => e
457
+ retries += 1
458
+
459
+ if retries <= max_retries
460
+ delay = calculate_backoff_delay(retries, e)
461
+ puts "[Rate Limit] Retry #{retries}/#{max_retries} in #{delay.round(2)}s"
462
+ sleep(delay)
463
+ retry
464
+ else
465
+ raise
466
+ end
467
+ end
468
+ end
469
+
470
+ private
471
+
472
+ def enforce_rate_limit
473
+ return unless @last_request_time
474
+
475
+ elapsed = Time.now - @last_request_time
476
+ sleep_time = @min_delay - elapsed
477
+
478
+ sleep(sleep_time) if sleep_time > 0
479
+ end
480
+
481
+ def calculate_backoff_delay(retries, error)
482
+ # Use API-provided retry timing if available
483
+ return error.error_data["retry_in_seconds"] if error.error_data["retry_in_seconds"]
484
+
485
+ # Otherwise use exponential backoff
486
+ base_delay = 2
487
+ max_delay = 60
488
+ delay = [base_delay * (2 ** (retries - 1)), max_delay].min
489
+
490
+ # Add jitter
491
+ jitter = delay * 0.25 * (rand - 0.5) * 2
492
+ delay + jitter
493
+ end
494
+ end
495
+
496
+ # Usage
497
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
498
+ limiter = MondayRateLimiter.new(client, requests_per_minute: 30)
499
+
500
+ # Create multiple items with rate limiting
501
+ item_names = ["Task 1", "Task 2", "Task 3", "Task 4", "Task 5"]
502
+ board_id = 1234567890
503
+
504
+ item_names.each do |item_name|
505
+ response = limiter.execute do |c|
506
+ c.item.create(
507
+ args: { board_id: board_id, item_name: item_name },
508
+ select: ["id", "name"]
509
+ )
510
+ end
511
+
512
+ if response.success?
513
+ item = response.body.dig("data", "create_item")
514
+ puts "Created: #{item['name']}"
515
+ end
516
+ end
517
+ ```
518
+
519
+ ## Queue-Based Rate Limiting
520
+
521
+ Process requests in a queue with controlled throughput:
522
+
523
+ ```ruby
524
+ require "monday_ruby"
525
+ require "thread"
526
+
527
+ class MondayRequestQueue
528
+ def initialize(client, max_requests_per_minute: 60)
529
+ @client = client
530
+ @queue = Queue.new
531
+ @max_requests_per_minute = max_requests_per_minute
532
+ @delay_between_requests = 60.0 / max_requests_per_minute
533
+ @running = false
534
+ end
535
+
536
+ def start
537
+ return if @running
538
+
539
+ @running = true
540
+ @worker_thread = Thread.new { process_queue }
541
+ end
542
+
543
+ def stop
544
+ @running = false
545
+ @worker_thread&.join
546
+ end
547
+
548
+ def enqueue(&block)
549
+ result_queue = Queue.new
550
+ @queue << { block: block, result: result_queue }
551
+ result_queue.pop # Wait for result
552
+ end
553
+
554
+ private
555
+
556
+ def process_queue
557
+ while @running
558
+ begin
559
+ request = @queue.pop(true) # Non-blocking pop
560
+
561
+ # Execute the request
562
+ result = execute_with_retry(request[:block])
563
+ request[:result] << result
564
+
565
+ # Wait before next request
566
+ sleep(@delay_between_requests)
567
+
568
+ rescue ThreadError
569
+ # Queue is empty, sleep briefly
570
+ sleep(0.1)
571
+ end
572
+ end
573
+ end
574
+
575
+ def execute_with_retry(block, max_retries: 3)
576
+ retries = 0
577
+
578
+ begin
579
+ block.call(@client)
580
+
581
+ rescue Monday::RateLimitError => e
582
+ retries += 1
583
+
584
+ if retries <= max_retries
585
+ delay = e.error_data["retry_in_seconds"] || (2 ** retries)
586
+ sleep(delay)
587
+ retry
588
+ else
589
+ raise
590
+ end
591
+ end
592
+ end
593
+ end
594
+
595
+ # Usage
596
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
597
+ queue = MondayRequestQueue.new(client, max_requests_per_minute: 30)
598
+ queue.start
599
+
600
+ # Enqueue multiple requests
601
+ board_ids = [1234567890, 2345678901, 3456789012]
602
+ boards = []
603
+
604
+ board_ids.each do |board_id|
605
+ response = queue.enqueue do |c|
606
+ c.board.query(
607
+ args: { ids: [board_id] },
608
+ select: ["id", "name"]
609
+ )
610
+ end
611
+
612
+ if response.success?
613
+ board = response.body.dig("data", "boards", 0)
614
+ boards << board
615
+ puts "Queued and fetched: #{board['name']}"
616
+ end
617
+ end
618
+
619
+ queue.stop
620
+ puts "Total boards: #{boards.length}"
621
+ ```
622
+
623
+ ## Best Practices
624
+
625
+ ### 1. Monitor Your Usage
626
+
627
+ Track API calls and complexity in your application:
628
+
629
+ ```ruby
630
+ require "monday_ruby"
631
+
632
+ class MondayMetrics
633
+ attr_reader :total_requests, :total_errors, :rate_limit_errors
634
+
635
+ def initialize(client)
636
+ @client = client
637
+ @total_requests = 0
638
+ @total_errors = 0
639
+ @rate_limit_errors = 0
640
+ end
641
+
642
+ def execute(&block)
643
+ @total_requests += 1
644
+ start_time = Time.now
645
+
646
+ begin
647
+ response = block.call(@client)
648
+ duration = Time.now - start_time
649
+
650
+ log_request(duration, response)
651
+ response
652
+
653
+ rescue Monday::RateLimitError => e
654
+ @rate_limit_errors += 1
655
+ @total_errors += 1
656
+ log_error(e, :rate_limit)
657
+ raise
658
+
659
+ rescue Monday::Error => e
660
+ @total_errors += 1
661
+ log_error(e, :api_error)
662
+ raise
663
+ end
664
+ end
665
+
666
+ def stats
667
+ {
668
+ total_requests: @total_requests,
669
+ total_errors: @total_errors,
670
+ rate_limit_errors: @rate_limit_errors,
671
+ success_rate: success_rate
672
+ }
673
+ end
674
+
675
+ private
676
+
677
+ def success_rate
678
+ return 0 if @total_requests.zero?
679
+ (((@total_requests - @total_errors).to_f / @total_requests) * 100).round(2)
680
+ end
681
+
682
+ def log_request(duration, response)
683
+ puts "[REQUEST] Completed in #{duration.round(3)}s - Status: #{response.status}"
684
+ end
685
+
686
+ def log_error(error, type)
687
+ puts "[ERROR:#{type}] #{error.class}: #{error.message}"
688
+ end
689
+ end
690
+
691
+ # Usage
692
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
693
+ metrics = MondayMetrics.new(client)
694
+
695
+ # Make several requests
696
+ 5.times do |i|
697
+ begin
698
+ response = metrics.execute do |c|
699
+ c.board.query(args: { limit: 10 })
700
+ end
701
+ puts "Request #{i + 1} succeeded"
702
+ rescue Monday::Error => e
703
+ puts "Request #{i + 1} failed: #{e.message}"
704
+ end
705
+
706
+ sleep(0.5)
707
+ end
708
+
709
+ puts "\nFinal Stats:"
710
+ puts metrics.stats
711
+ ```
712
+
713
+ ### 2. Cache API Responses
714
+
715
+ Reduce API calls by caching responses:
716
+
717
+ ```ruby
718
+ require "monday_ruby"
719
+
720
+ class MondayCache
721
+ def initialize(client, ttl: 300)
722
+ @client = client
723
+ @cache = {}
724
+ @ttl = ttl # Time to live in seconds
725
+ end
726
+
727
+ def get_board(board_id, select: ["id", "name"])
728
+ cache_key = "board_#{board_id}_#{select.hash}"
729
+
730
+ # Check cache
731
+ if cached = get_from_cache(cache_key)
732
+ puts "[CACHE HIT] Board #{board_id}"
733
+ return cached
734
+ end
735
+
736
+ # Fetch from API
737
+ puts "[CACHE MISS] Fetching board #{board_id}"
738
+ response = @client.board.query(
739
+ args: { ids: [board_id] },
740
+ select: select
741
+ )
742
+
743
+ if response.success?
744
+ board = response.body.dig("data", "boards", 0)
745
+ set_in_cache(cache_key, board)
746
+ board
747
+ end
748
+ end
749
+
750
+ def clear_cache
751
+ @cache.clear
752
+ end
753
+
754
+ private
755
+
756
+ def get_from_cache(key)
757
+ entry = @cache[key]
758
+ return nil unless entry
759
+
760
+ # Check if expired
761
+ if Time.now - entry[:timestamp] > @ttl
762
+ @cache.delete(key)
763
+ return nil
764
+ end
765
+
766
+ entry[:data]
767
+ end
768
+
769
+ def set_in_cache(key, data)
770
+ @cache[key] = {
771
+ data: data,
772
+ timestamp: Time.now
773
+ }
774
+ end
775
+ end
776
+
777
+ # Usage
778
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
779
+ cache = MondayCache.new(client, ttl: 600) # 10 minute cache
780
+
781
+ # First request - cache miss
782
+ board1 = cache.get_board(1234567890)
783
+ puts "Board: #{board1['name']}"
784
+
785
+ # Second request - cache hit (no API call)
786
+ board2 = cache.get_board(1234567890)
787
+ puts "Board: #{board2['name']}"
788
+ ```
789
+
790
+ ### 3. Optimize Query Depth
791
+
792
+ Avoid deeply nested queries:
793
+
794
+ ```ruby
795
+ require "monday_ruby"
796
+
797
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
798
+
799
+ # ❌ Deeply nested (high complexity)
800
+ response = client.board.query(
801
+ args: { ids: [1234567890] },
802
+ select: [
803
+ "id", "name",
804
+ {
805
+ items: [
806
+ "id", "name",
807
+ {
808
+ column_values: ["id", "text", "value"],
809
+ updates: [
810
+ "id", "body",
811
+ { replies: ["id", "body"] }
812
+ ]
813
+ }
814
+ ]
815
+ }
816
+ ]
817
+ )
818
+
819
+ # ✅ Shallow queries (lower complexity)
820
+ # First, get the board and items
821
+ response1 = client.board.query(
822
+ args: { ids: [1234567890] },
823
+ select: ["id", "name", { items: ["id", "name"] }]
824
+ )
825
+
826
+ board = response1.body.dig("data", "boards", 0)
827
+ item_ids = board["items"].map { |i| i["id"] }
828
+
829
+ # Then, get item details separately if needed
830
+ response2 = client.item.query(
831
+ args: { ids: item_ids.take(10) }, # Process in batches
832
+ select: ["id", "name", { column_values: ["id", "text"] }]
833
+ )
834
+ ```
835
+
836
+ ### 4. Use Environment-Based Limits
837
+
838
+ Configure rate limits based on environment:
839
+
840
+ ```ruby
841
+ require "monday_ruby"
842
+
843
+ class MondayClientFactory
844
+ def self.create(environment: :production)
845
+ client = Monday::Client.new(token: ENV["MONDAY_TOKEN"])
846
+
847
+ config = rate_limit_config(environment)
848
+ MondayRateLimiter.new(client, **config)
849
+ end
850
+
851
+ def self.rate_limit_config(environment)
852
+ case environment
853
+ when :production
854
+ { requests_per_minute: 30 } # Conservative for production
855
+ when :development
856
+ { requests_per_minute: 10 } # Very conservative for dev
857
+ when :testing
858
+ { requests_per_minute: 5 } # Minimal for tests
859
+ else
860
+ { requests_per_minute: 20 } # Default
861
+ end
862
+ end
863
+ end
864
+
865
+ # Usage
866
+ limiter = MondayClientFactory.create(environment: :production)
867
+
868
+ response = limiter.execute do |c|
869
+ c.board.query(args: { limit: 10 })
870
+ end
871
+ ```
872
+
873
+ ## Troubleshooting
874
+
875
+ ### Rate Limit Exceeded Despite Delays
876
+
877
+ **Problem**: Still getting rate limit errors even with delays between requests.
878
+
879
+ **Solution**: Your queries may have high complexity. Reduce fields or use pagination:
880
+
881
+ ```ruby
882
+ # Instead of fetching all items at once
883
+ response = client.board.query(
884
+ args: { ids: [1234567890] },
885
+ select: ["id", "name", { items: ["id", "name"] }]
886
+ )
887
+
888
+ # Use pagination
889
+ response = client.board.query(
890
+ args: { ids: [1234567890] },
891
+ select: [
892
+ "id", "name",
893
+ {
894
+ items_page: {
895
+ args: { limit: 25 },
896
+ select: [{ items: ["id", "name"] }]
897
+ }
898
+ }
899
+ ]
900
+ )
901
+ ```
902
+
903
+ ### Complexity Budget Exhausted
904
+
905
+ **Problem**: Getting `COMPLEXITY_BUDGET_EXHAUSTED` error.
906
+
907
+ **Solution**: Check your remaining budget before expensive operations:
908
+
909
+ ```ruby
910
+ query = <<~GRAPHQL
911
+ query {
912
+ complexity {
913
+ before
914
+ reset_in_x_seconds
915
+ }
916
+ }
917
+ GRAPHQL
918
+
919
+ response = client.make_request(query)
920
+ complexity = response.body.dig("data", "complexity")
921
+
922
+ if complexity["before"] < 100000
923
+ puts "Low budget. Waiting #{complexity['reset_in_x_seconds']}s..."
924
+ sleep(complexity["reset_in_x_seconds"])
925
+ end
926
+
927
+ # Now make your request
928
+ ```
929
+
930
+ ### Concurrent Requests Causing Errors
931
+
932
+ **Problem**: Multiple threads/processes hitting rate limits.
933
+
934
+ **Solution**: Use a centralized queue or distributed rate limiter (Redis-based):
935
+
936
+ ```ruby
937
+ require "redis"
938
+
939
+ class DistributedRateLimiter
940
+ def initialize(client, redis_url: "redis://localhost:6379")
941
+ @client = client
942
+ @redis = Redis.new(url: redis_url)
943
+ @key = "monday_api_rate_limit"
944
+ end
945
+
946
+ def execute(&block)
947
+ wait_for_token
948
+
949
+ begin
950
+ block.call(@client)
951
+ ensure
952
+ # Record request timestamp
953
+ @redis.zadd(@key, Time.now.to_i, SecureRandom.uuid)
954
+ @redis.expire(@key, 60)
955
+ end
956
+ end
957
+
958
+ private
959
+
960
+ def wait_for_token
961
+ loop do
962
+ # Count requests in last 60 seconds
963
+ now = Time.now.to_i
964
+ count = @redis.zcount(@key, now - 60, now)
965
+
966
+ if count < 30 # Max 30 requests per minute
967
+ break
968
+ else
969
+ sleep(1)
970
+ end
971
+ end
972
+ end
973
+ end
974
+ ```
975
+
976
+ ## Next Steps
977
+
978
+ - [Error handling guide](/guides/advanced/errors)
979
+ - [Optimize query performance](/guides/boards/query)
980
+ - [Pagination strategies](/guides/items/query)
981
+ - [monday.com API rate limits documentation](https://developer.monday.com/api-reference/docs/rate-limits)