flowbase-cli 0.1.13__tar.gz → 0.1.15__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -125,6 +125,7 @@ flowbase docs catalogs permissions # function permission points + data sc
125
125
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
126
126
  flowbase docs catalogs page-components # Page component shapes, placement, fields, and enums
127
127
  flowbase docs catalogs page-rules # Page condition operators and action shapes
128
+ flowbase docs catalogs forms # custom form fields, rules, appearance, and settings
128
129
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
129
130
  flowbase docs catalogs icons # icon guidance + examples
130
131
  ```
@@ -279,3 +280,12 @@ flowbase roles update rol_xxx --json-file role.json
279
280
  - `docs catalogs page-buttons`, `workflow-branches`, `approval-rules`, and `dashboard-runtime` describe complex configuration and runtime limits.
280
281
 
281
282
  Transport requests time out after 60 seconds. A lost write response has an unknown outcome: read the resource and current revision before retrying. The CLI never automatically retries writes.
283
+
284
+ ### Recent feature authoring
285
+
286
+ - `flowbase docs catalogs planning`: tree/Gantt view snapshots, candidate queries and optimistic plan updates. Read a full view with `views list` before updating; preserve `displayMode`, `parentFieldId` and `gantt`.
287
+ - `flowbase docs catalogs record-bundles`: initialize, atomically submit and recover a parent/detail submission. Reuse a submission ID only with the identical request body.
288
+ - `flowbase docs catalogs formula-authoring`: formula/rollup sources, validation, saved-record trials and runtime draft preview. Derived values are read-only.
289
+ - `flowbase docs catalogs workflow-python`: record creation examples and failed-task retry. `workflows python-runtime OBJECT new` discovers current object bindings; `workflows executions` includes Python attempts, logs and `canRetry`.
290
+
291
+ Use `records list OBJECT --view-id VIEW --view-layout` for tree/Gantt projections; the default remains a flat list. `records update` accepts `updatedAt` and `planViewId` in its JSON body for version-checked plan edits. Formula validation/trials require both metadata read and record read; workflow retry requires both metadata write and record write and explicit CLI confirmation (`--yes` in automation).
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: flowbase-cli
3
- Version: 0.1.13
4
- Summary: Command line client for the FlowBase OpenAPI
5
- Author: DeepFlowAI
6
- License: Apache-2.0
7
- Requires-Python: >=3.11
8
- Description-Content-Type: text/markdown
9
-
10
1
  # FlowBase CLI
11
2
 
12
3
  `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, approval flows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
@@ -125,6 +116,7 @@ flowbase docs catalogs permissions # function permission points + data sc
125
116
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
126
117
  flowbase docs catalogs page-components # Page component shapes, placement, fields, and enums
127
118
  flowbase docs catalogs page-rules # Page condition operators and action shapes
119
+ flowbase docs catalogs forms # custom form fields, rules, appearance, and settings
128
120
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
129
121
  flowbase docs catalogs icons # icon guidance + examples
130
122
  ```
@@ -279,3 +271,12 @@ flowbase roles update rol_xxx --json-file role.json
279
271
  - `docs catalogs page-buttons`, `workflow-branches`, `approval-rules`, and `dashboard-runtime` describe complex configuration and runtime limits.
280
272
 
281
273
  Transport requests time out after 60 seconds. A lost write response has an unknown outcome: read the resource and current revision before retrying. The CLI never automatically retries writes.
274
+
275
+ ### Recent feature authoring
276
+
277
+ - `flowbase docs catalogs planning`: tree/Gantt view snapshots, candidate queries and optimistic plan updates. Read a full view with `views list` before updating; preserve `displayMode`, `parentFieldId` and `gantt`.
278
+ - `flowbase docs catalogs record-bundles`: initialize, atomically submit and recover a parent/detail submission. Reuse a submission ID only with the identical request body.
279
+ - `flowbase docs catalogs formula-authoring`: formula/rollup sources, validation, saved-record trials and runtime draft preview. Derived values are read-only.
280
+ - `flowbase docs catalogs workflow-python`: record creation examples and failed-task retry. `workflows python-runtime OBJECT new` discovers current object bindings; `workflows executions` includes Python attempts, logs and `canRetry`.
281
+
282
+ Use `records list OBJECT --view-id VIEW --view-layout` for tree/Gantt projections; the default remains a flat list. `records update` accepts `updatedAt` and `planViewId` in its JSON body for version-checked plan edits. Formula validation/trials require both metadata read and record read; workflow retry requires both metadata write and record write and explicit CLI confirmation (`--yes` in automation).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowbase-cli"
7
- version = "0.1.13"
7
+ version = "0.1.15"
8
8
  description = "Command line client for the FlowBase OpenAPI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """FlowBase OpenAPI CLI."""
2
2
 
3
- __version__ = "0.1.13"
3
+ __version__ = "0.1.15"
@@ -22,6 +22,17 @@ class Endpoint:
22
22
 
23
23
 
24
24
  ENDPOINTS = (
25
+ Endpoint('formulas.sources', 'flowbase_formulas_sources', 'GET', '/objects/{object_id}/formula/sources', 'metadata:read', 'Discover scalar formula sources in the draft.', 'flowbase formulas sources obj_xxx'),
26
+ Endpoint('formulas.collections', 'flowbase_formulas_collections', 'GET', '/objects/{object_id}/formula/collections', 'metadata:read', 'Discover collection object and field IDs in the draft.', 'flowbase formulas collections obj_xxx'),
27
+ Endpoint('formulas.validate', 'flowbase_formulas_validate', 'POST', '/objects/{object_id}/formula/validate', 'metadata:read', 'Validate formula or rollup; trial also supported. Requires records:read. No writes.', 'flowbase formulas validate obj_xxx --json-file formula.json', request_body='{"constraints":{"expression":"amount * 2","resultType":"number","scale":2},"samples":{"amount":12.50},"trial":true}'),
28
+ Endpoint('records.formula-preview', 'flowbase_records_formula_preview', 'POST', '/objects/{object_id}/formula-preview', 'records:read', 'Preview runtime formulas without saving. New rollups remain pending; saved rollups use saved data.', 'flowbase records formula-preview obj_xxx --json-file preview.json', request_body='{"values":{"amount":12.50}}'),
29
+ Endpoint('records.bundle-init', 'flowbase_records_bundle_init', 'GET', '/objects/{object_id}/bundle/init', 'records:read', 'Load configuration and collection versions before an atomic write.', 'flowbase records bundle-init obj_xxx --record-id rec_xxx'),
30
+ Endpoint('records.bundle-save', 'flowbase_records_bundle_save', 'POST', '/objects/{object_id}/bundle', 'records:write', 'Atomically save parent and configured related rows. Reuse submissionId only for identical retries.', 'flowbase records bundle-save obj_xxx --json-file bundle.json', request_body='{"submissionId":"unique-submission-0001","configVersion":"from-bundle-init","values":{"name":"Order"},"components":[]}'),
31
+ Endpoint('records.bundle-status', 'flowbase_records_bundle_status', 'GET', '/objects/{object_id}/bundle/submissions/{submission_id}', 'records:read', "Query this API key's submission receipt after a timeout; unknown does not prove failure.", 'flowbase records bundle-status obj_xxx unique-submission-0001'),
32
+ Endpoint('records.hierarchy-candidates', 'flowbase_records_hierarchy_candidates', 'GET', '/objects/{object_id}/hierarchy/candidates', 'records:read', 'Discover eligible parent records, excluding self and descendants.', 'flowbase records hierarchy-candidates obj_xxx --field fld_parent --exclude rec_xxx'),
33
+ Endpoint('records.gantt-candidates', 'flowbase_records_gantt_candidates', 'GET', '/objects/{object_id}/gantt/candidates', 'records:read', 'Discover eligible predecessors, excluding cycles and hierarchy conflicts.', 'flowbase records gantt-candidates obj_xxx --field fld_predecessors --exclude rec_xxx'),
34
+ Endpoint('workflows.python-runtime', 'flowbase_workflows_python_runtime', 'GET', '/objects/{object_id}/workflows/{workflow_id}/python-runtime', 'metadata:read', 'Discover Python APIs and creation methods. Use workflow ID new before creation.', 'flowbase workflows python-runtime obj_xxx new'),
35
+ Endpoint('workflows.retry', 'flowbase_workflows_retry', 'POST', '/objects/{object_id}/workflows/{workflow_id}/executions/{execution_id}/retry', 'metadata:write', 'Retry a failed Python execution; also requires records:write.', 'flowbase workflows retry obj_xxx wfl_xxx exe_xxx --yes', requires_confirmation=True),
25
36
  Endpoint("pages.evaluate", "flowbase_pages_evaluate", "POST", "/pages/{page_id}/evaluate", "metadata:read", "Evaluate draft page rule effects on sample field-key values; no writes or visual rendering.", "flowbase pages evaluate pag_xxx --json-file values.json", request_body='{"values":{"done":true}}'),
26
37
  Endpoint("dashboards.run", "flowbase_dashboards_run", "POST", "/dashboards/{dashboard_id}/components/{component_id}/run", "metadata:read", "Run a saved draft component; also requires records:read. Inspect ok/output/error, not visual rendering.", "flowbase dashboards run dsh_xxx dcm_xxx --json-file filters.json", request_body='{"filterValues":{}}'),
27
38
  Endpoint("organization.employees", "flowbase_organization_employees", "GET", "/organization/employees", "metadata:read", "Paginated employee IDs/names; follow pages with --page, optionally --query.", "flowbase organization employees --page 1"),
@@ -169,6 +180,26 @@ ENDPOINTS = (
169
180
  )),
170
181
  Endpoint("dashboards.copy", "flowbase_dashboards_copy", "POST", "/dashboards/{dashboard_id}/copy", "metadata:write", "Copy a dashboard (not added to any app).", "flowbase dashboards copy dsh_xxx"),
171
182
  Endpoint("dashboards.delete", "flowbase_dashboards_delete", "POST", "/dashboards/{dashboard_id}/delete", "metadata:write", "Logically delete a dashboard (also removed from all apps).", "flowbase dashboards delete dsh_xxx --yes", True),
183
+ Endpoint("forms.list", "flowbase_forms_list", "GET", "/forms", "metadata:read", "List custom forms with collection status.", "flowbase forms list"),
184
+ Endpoint("forms.get", "flowbase_forms_get", "GET", "/forms/{form_id}", "metadata:read", "Get a custom form definition and its placeable field options. Read docs catalogs forms before update.", "flowbase forms get frm_xxx"),
185
+ Endpoint("forms.create", "flowbase_forms_create", "POST", "/forms", "metadata:write", "Create a custom form draft; object-required fields are pre-added.", "flowbase forms create --label 'Website inquiry' --key website_inquiry --object-id obj_xxx",
186
+ request_body='{"label":"Website inquiry","key":"website_inquiry","objectId":"obj_xxx","description":""}'),
187
+ Endpoint("forms.update", "flowbase_forms_update", "PUT", "/forms/{form_id}", "metadata:write", "Update a custom form draft (full snapshot: fields, rules, appearance, settings). Read docs catalogs forms first.", "flowbase forms update frm_xxx --json-file form.json",
188
+ request_body=(
189
+ '{"label":"Website inquiry","description":"","baseRevision":3,'
190
+ '"fields":[{"fieldId":"fld_xxx","label":"","description":"","state":"required","prefill":false}],'
191
+ '"rules":[],'
192
+ '"appearance":{"title":"Website inquiry","description":"","logo":null,"cover":null,"themeColor":"#171717",'
193
+ '"background":"gray","submitText":"提交","language":"zh-CN"},'
194
+ '"settings":{"afterSubmit":"message","successTitle":"提交成功","successBody":"感谢你的填写",'
195
+ '"allowResubmit":true,"redirectUrl":"","deadline":null,"limit":null}}'
196
+ )),
197
+ Endpoint("forms.copy", "flowbase_forms_copy", "POST", "/forms/{form_id}/copy", "metadata:write", "Copy a custom form (collection state is not copied).", "flowbase forms copy frm_xxx"),
198
+ Endpoint("forms.delete", "flowbase_forms_delete", "POST", "/forms/{form_id}/delete", "metadata:write", "Logically delete a custom form.", "flowbase forms delete frm_xxx --yes", True),
199
+ Endpoint("forms.share.get", "flowbase_forms_share_get", "GET", "/forms/{form_id}/share", "metadata:read", "Get the public link and collection state in this environment.", "flowbase forms share frm_xxx"),
200
+ Endpoint("forms.share.set-accepting", "flowbase_forms_share_set_accepting", "PUT", "/forms/{form_id}/share/accepting", "metadata:write", "Turn public collection on or off in this environment (immediate, not a draft change).", "flowbase forms set-accepting frm_xxx --on",
201
+ request_body='{"accepting":true}'),
202
+ Endpoint("forms.share.reset", "flowbase_forms_share_reset", "POST", "/forms/{form_id}/share/reset", "metadata:write", "Reset the public link; the old link stops working immediately.", "flowbase forms reset-link frm_xxx --yes", True),
172
203
  Endpoint("objects.list", "flowbase_objects_list", "GET", "/objects", "metadata:read", "List object metadata.", "flowbase objects list"),
173
204
  Endpoint("objects.get", "flowbase_objects_get", "GET", "/objects/{object_id}", "metadata:read", "Get object and field metadata.", "flowbase objects get obj_xxx"),
174
205
  Endpoint("objects.create", "flowbase_objects_create", "POST", "/objects", "metadata:write", "Create an object draft.", "flowbase objects create --json-file object.json",
@@ -225,15 +256,15 @@ ENDPOINTS = (
225
256
  '"filters":['
226
257
  '{"fieldId":"fld_status","operator":"eq","value":"opt_xxx"},'
227
258
  '{"fieldId":"fld_done","operator":"is_true"}'
228
- '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null}'
259
+ '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null,"displayMode":"table","parentFieldId":null,"gantt":{}}'
229
260
  )),
230
- Endpoint("views.update", "flowbase_views_update", "PUT", "/objects/{object_id}/views/{view_id}", "metadata:write", "Update an object view draft.", "flowbase views update obj_xxx viw_xxx --json-file view.json",
261
+ Endpoint("views.update", "flowbase_views_update", "PUT", "/objects/{object_id}/views/{view_id}", "metadata:write", "Update a complete view snapshot. Preserve displayMode, parentFieldId and gantt from views.list; omission resets to table.", "flowbase views update obj_xxx viw_xxx --json-file view.json",
231
262
  request_body=(
232
263
  '{"baseRevision":4,"name":"Active","enabled":true,"match":"all",'
233
264
  '"filters":['
234
265
  '{"fieldId":"fld_status","operator":"eq","value":"opt_xxx"},'
235
266
  '{"fieldId":"fld_done","operator":"is_true"}'
236
- '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null}'
267
+ '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null,"displayMode":"table","parentFieldId":null,"gantt":{}}'
237
268
  )),
238
269
  Endpoint("views.reorder", "flowbase_views_reorder", "PUT", "/objects/{object_id}/views/order", "metadata:write", "Reorder object views.", "flowbase views reorder obj_xxx --json '{\"baseRevision\":4,\"viewIds\":[\"viw_xxx\"]}'",
239
270
  request_body='{"baseRevision":4,"viewIds":["viw_a","viw_b"]}'),
@@ -563,7 +594,8 @@ ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
563
594
 
564
595
  # Closed-domain catalogs for offline authoring of complex request bodies.
565
596
  # Keep in sync with server registries (field types, workflow/approval node kinds,
566
- # permission points, dashboard filter presets, page component kinds).
597
+ # permission points, dashboard filter presets, page component kinds, form
598
+ # snapshot validation).
567
599
  # Icons intentionally reference the server file because the full Tabler set is large.
568
600
  _PERMISSIONS = ("required", "optional", "readonly", "hidden")
569
601
  _TEXT_FILTERS = ("eq", "neq", "contains", "not_contains", "is_empty", "is_not_empty")
@@ -604,6 +636,17 @@ def _field_type(
604
636
 
605
637
 
606
638
  FIELD_TYPES = (
639
+ _field_type(
640
+ "rollup", ("source", "operation", "resultType", "scale", "currency", "unit"),
641
+ writable=False, record_value="derived scalar | null; omit on writes; see formulaStates",
642
+ extra={"permissions": ["readonly", "hidden"], "operations": ["sum", "avg", "count", "count_values", "min", "max"], "sourceModes": ["reverse", "forward", "children"]},
643
+ ),
644
+ _field_type(
645
+ "formula", ("expression", "resultType", "scale", "currency", "bindings", "languageVersion", "collections"),
646
+ writable=False, record_value="derived scalar | null; omit on writes; see formulaStates for failures",
647
+ extra={"permissions": ["readonly", "hidden"], "resultTypes": ["number", "number.currency", "number.percent", "text.singleLine", "date", "boolean"],
648
+ "createExample": {"label": "Total", "key": "total", "description": "Calculated total", "type": "formula", "typeVersion": "v1", "permission": "readonly", "constraints": {"expression": "quantity * unit_price", "resultType": "number", "scale": 2}}},
649
+ ),
607
650
  _field_type(
608
651
  "text.singleLine", ("maxLength", "minLength", "placeholder", "unique"),
609
652
  filter_operators=_TEXT_FILTERS, record_value="string | null",
@@ -802,7 +845,10 @@ AUTHORING = {
802
845
  ],
803
846
  },
804
847
  "documentationKeys": "docs list returns endpoint keys. Use apps.objects.add for apps add-objects; docs show includes the CLI example.",
805
- "requiredUpdateFields": {"apps.update": ["label", "icon", "color"]},
848
+ "requiredUpdateFields": {
849
+ "apps.update": ["label", "icon", "color"],
850
+ "forms.update": ["label", "appearance", "settings"],
851
+ },
806
852
  "requiredCreateFields": {
807
853
  "objects.create": ["label", "key", "description"],
808
854
  "fields.create": ["label", "key", "description", "type"],
@@ -811,22 +857,27 @@ AUTHORING = {
811
857
  "pages.create": ["baseRevision", "page"],
812
858
  "records.create": ["values"],
813
859
  "print-templates.create": ["name", "html"],
860
+ "forms.create": ["label", "key", "objectId"],
814
861
  },
815
862
  "draftRevision": {
816
863
  "notReturnedBy": ["objects.get", "objects.list", "apps.get", "apps.list"],
817
864
  "returnedBy": [
818
865
  "views.list", "pages.list", "pages.get",
819
866
  "print-templates.list", "print-templates.get",
867
+ "forms.get",
820
868
  "every metadata write response (draftRevision)",
821
869
  ],
822
870
  "note": "Capture draftRevision from the last write or from views.list/pages.list/print-templates.list before writes.",
823
871
  },
824
872
  "readThenWrite": (
825
- "For update of views/pages/dashboards/fields with constraints: get the snapshot, "
826
- "edit it, set baseRevision from draftRevision. fields.update.constraints is a full replace; "
827
- "sending {} wipes options. print-templates.update is partial: send only changed fields, "
828
- "baseRevision, and baseUpdatedAt copied from updatedAt. Resending id, key, enabled, "
829
- "updatedAt, or draftRevision is rejected."
873
+ "For update of views/pages/dashboards/forms/fields with constraints: get the snapshot, "
874
+ "edit it, set baseRevision from draftRevision. forms.update unwraps forms.get: send "
875
+ "label, description, fields, rules, appearance, settings, and baseRevision; omit the "
876
+ "form wrapper, id, key, objectId, updatedAt, and fieldOptions. Omitting fields or rules "
877
+ "clears them. fields.update.constraints is a full replace; sending {} wipes options. "
878
+ "print-templates.update is partial: send only changed fields, baseRevision, and "
879
+ "baseUpdatedAt copied from updatedAt. Resending id, key, enabled, updatedAt, or "
880
+ "draftRevision is rejected."
830
881
  ),
831
882
  "recordsNeedReleasedMetadata": (
832
883
  "On production, records.create/list/get use the released runtime, not the current draft. "
@@ -840,6 +891,7 @@ AUTHORING = {
840
891
  "dashboards.run": "{ok,output,error,logs,durationMs,cached,mode:draft-preview}; inspect ok and error.",
841
892
  "apps.create": "{appId,draftRevision}",
842
893
  "dashboards.create": "{dashboardId,draftRevision}",
894
+ "forms.create": "{formId,draftRevision}",
843
895
  "workflows.create": "{workflowId,draftRevision}",
844
896
  "approvals.create": "{approvalId,draftRevision}",
845
897
  "print-templates.create": "{templateId,draftRevision}",
@@ -876,6 +928,10 @@ AUTHORING = {
876
928
  "wrong": "select/lookup filter or record value uses the display label",
877
929
  "right": "Use opt_/rec_/emp_ ids from objects.get / records.get.",
878
930
  },
931
+ {
932
+ "wrong": "forms update with the forms.get body, or fields omitted",
933
+ "right": "Unwrap form, keep every placed field, and set baseRevision from draftRevision. See docs catalogs forms.",
934
+ },
879
935
  {
880
936
  "wrong": "records.list.values is the full record",
881
937
  "right": "List is a column projection. records.get for all field keys.",
@@ -984,7 +1040,7 @@ APPROVAL_NODE_TYPES = (
984
1040
  )
985
1041
 
986
1042
  FUNCTION_PERMISSIONS = (
987
- "workspace.access", "app.manage", "object.manage", "dashboard.manage",
1043
+ "workspace.access", "app.manage", "object.manage", "dashboard.manage", "form.manage",
988
1044
  "version.manage", "org.manage", "system.manage",
989
1045
  )
990
1046
 
@@ -1065,7 +1121,8 @@ PAGE_COMPONENTS = {
1065
1121
  "kind", "label", "hideLabel", "objectId", "relationFieldId",
1066
1122
  "columnFieldIds", "sort", "limit", "showViewAll", "createAction",
1067
1123
  ),
1068
- "optionalKeys": ("id", "filters", "carryIns"),
1124
+ "optionalKeys": ("id", "filters", "carryIns", "editMode", "editOptions", "editColumns"),
1125
+ "editMode": {"withParent": "Atomic editing on create/detail pages; initialize with records bundle-init, then bundle-save."},
1069
1126
  "limit": (5, 10, 20),
1070
1127
  "sortDirections": ("asc", "desc"),
1071
1128
  "carryInSources": (
@@ -1285,7 +1342,8 @@ RESOURCE_ID_PREFIXES = {
1285
1342
  "reg_": "page region", "cmp_": "page component",
1286
1343
  "dsh_": "dashboard", "dcm_": "dashboard component", "rol_": "role",
1287
1344
  "rec_": "record", "att_": "attachment", "emp_": "employee", "dep_": "department",
1288
- "ptp_": "print template",
1345
+ "ptp_": "print template", "frm_": "custom form", "fim_": "form image",
1346
+ "rule_": "form or page rule",
1289
1347
  }
1290
1348
 
1291
1349
  WORKFLOW_BRANCHES = {
@@ -1379,7 +1437,136 @@ PRINT_TEMPLATES = {
1379
1437
  "verify": "The management CLI cannot render a template. get returns the source. A saved {{ }} that names the wrong key prints blank. There is no print-templates run command.",
1380
1438
  }
1381
1439
 
1440
+ FORMS = {
1441
+ "command": "flowbase docs catalogs forms",
1442
+ "readFirst": (
1443
+ "forms get returns form, fieldOptions, and draftRevision. "
1444
+ "fieldOptions[].availability is the placement contract: placeable, reason, states, "
1445
+ "preset, presetRequired, prefill, plus objectRequired and usableDefault. "
1446
+ "Use field IDs from fieldOptions, never field keys."
1447
+ ),
1448
+ "write": {
1449
+ "create": (
1450
+ "Requires label, key, and objectId. description defaults to empty. "
1451
+ "Placeable object-required fields are pre-added. key starts with a lowercase letter, "
1452
+ "contains only lowercase letters, digits, and underscores, max 50, and cannot change later."
1453
+ ),
1454
+ "update": (
1455
+ "Full snapshot. Requires label, appearance, and settings. "
1456
+ "fields and rules default to [] and clear the form when omitted. "
1457
+ "Copy draftRevision to baseRevision. Unwrap forms.get: do not send the form wrapper, "
1458
+ "id, key, objectId, updatedAt, or fieldOptions."
1459
+ ),
1460
+ "channel": (
1461
+ "set-accepting and reset-link take effect immediately in this environment and are not "
1462
+ "draft changes. copy does not copy collection state or the public link. delete and "
1463
+ "reset-link require --yes."
1464
+ ),
1465
+ },
1466
+ "limits": {
1467
+ "formsPerEnvironment": 200,
1468
+ "fields": 100,
1469
+ "rules": 50,
1470
+ "label": "1 to 50 characters",
1471
+ "description": "max 200 characters",
1472
+ "conditionsPerRule": "1 to 10",
1473
+ },
1474
+ "field": {
1475
+ "requiredKeys": ["fieldId", "label", "description", "state", "prefill"],
1476
+ "optionalKeys": ["preset"],
1477
+ "label": "Display override. Empty string keeps the object field label. Max 100.",
1478
+ "description": "Max 500. Empty string is allowed.",
1479
+ "state": (
1480
+ "One of availability.states. Ordinary fields use required, optional, readonly, hidden. "
1481
+ "lookup.record, lookup.record.multiple, lookup.employee, and lookup.department are "
1482
+ "hidden only. attachment cannot be readonly."
1483
+ ),
1484
+ "prefill": "true only when availability.prefill is true. Hidden-only, attachment, and text.rich cannot be prefilled.",
1485
+ "preset": (
1486
+ "Omit the key when there is no preset. Required when availability.presetRequired. "
1487
+ "A fixed value uses the record value shape from record-values. "
1488
+ "A dynamic preset is {\"mode\":\"dynamic\",\"value\":\"<dynamic default>\"}; currentEmployee is rejected."
1489
+ ),
1490
+ "objectRequired": (
1491
+ "Every placeable object-required field must stay in fields. It cannot be optional. "
1492
+ "readonly or hidden needs a preset unless usableDefault is true."
1493
+ ),
1494
+ "fillable": "At least one placed field is required or optional, or an enabled rule sets one of those states.",
1495
+ },
1496
+ "rule": {
1497
+ "requiredKeys": ["name", "enabled", "match", "conditions", "actions"],
1498
+ "optionalKeys": ["id"],
1499
+ "id": "Omit id, or send a value that does not start with rule_, to allocate a new rule_ id. Preserve an existing rule_ id on update.",
1500
+ "name": "1 to 50 characters, unique in the form.",
1501
+ "match": ["all", "any"],
1502
+ "conditions": (
1503
+ "Same operators as page-rules. Candidates are placed fields that are not "
1504
+ "lookup.record, lookup.record.multiple, lookup.employee, or lookup.department."
1505
+ ),
1506
+ "actions": (
1507
+ "Only setFieldState: {\"type\":\"setFieldState\",\"fieldId\":\"fld_...\",\"state\":\"required\"}. "
1508
+ "state must be in that field's availability.states. No component visibility."
1509
+ ),
1510
+ },
1511
+ "appearance": {
1512
+ "requiredKeys": [
1513
+ "title", "description", "logo", "cover", "themeColor",
1514
+ "background", "submitText", "language",
1515
+ ],
1516
+ "title": "1 to 100 characters.",
1517
+ "description": "Max 2000 characters.",
1518
+ "logo": "null or an existing fim_ id. The management CLI cannot upload images.",
1519
+ "cover": "null or an existing fim_ id. The management CLI cannot upload images.",
1520
+ "themeColor": "#RRGGBB",
1521
+ "background": ["gray", "white", "tint"],
1522
+ "submitText": "Max 20 characters. Empty string is allowed.",
1523
+ "language": ["zh-CN", "en"],
1524
+ },
1525
+ "settings": {
1526
+ "requiredKeys": [
1527
+ "afterSubmit", "successTitle", "successBody", "allowResubmit",
1528
+ "redirectUrl", "deadline", "limit",
1529
+ ],
1530
+ "afterSubmit": ["message", "redirect"],
1531
+ "successTitle": "Max 50 characters.",
1532
+ "successBody": "Max 500 characters.",
1533
+ "redirectUrl": "An https URL when afterSubmit is redirect, max 2048. Otherwise an empty string is allowed.",
1534
+ "deadline": "null, or an ISO-8601 datetime with a timezone. A newly set deadline cannot be in the past.",
1535
+ "limit": "null, or an integer from 1 to 1000000.",
1536
+ },
1537
+ "collectStatus": ["inactive", "invalid", "closed", "deadline", "full", "collecting"],
1538
+ "notInCli": (
1539
+ "Public form read, submit, and attachment upload need no API key and are not management commands. "
1540
+ "Logo and cover upload exist only on the signed-in admin API."
1541
+ ),
1542
+ }
1543
+
1382
1544
  CATALOGS = {
1545
+ "planning": {
1546
+ "viewUpdate": "Read views.list, preserve the complete snapshot, then views.update with baseRevision. Omitting displayMode/parentFieldId/gantt resets them to table/null/{}.",
1547
+ "tree": {"displayMode": "tree", "parentFieldId": "fld_parent", "gantt": {}},
1548
+ "gantt": {"displayMode": "gantt", "parentFieldId": None, "gantt": {"startFieldId": "fld_start", "endFieldId": "fld_end", "predecessorFieldId": "fld_dependencies"}},
1549
+ "read": "records list OBJECT --view-id VIEW --view-layout; use --parent, --root, --locate and --hierarchy-version for tree navigation. Default is flat.",
1550
+ "write": {"values": {"start": "2026-10-24", "end": "2026-10-25"}, "planViewId": "viw_plan", "updatedAt": "from-records-get"},
1551
+ "candidates": "records hierarchy-candidates / gantt-candidates use field IDs. Record values use field keys.",
1552
+ },
1553
+ "record-bundles": {
1554
+ "steps": ["Configure relatedList editMode=withParent on the default create/detail page.", "records bundle-init OBJECT [--record-id RECORD] returns configVersion, updatedAt and component collection versions.", "records bundle-save OBJECT --json-file bundle.json commits the entire write set or rolls back.", "After a timeout, bundle-status OBJECT SUBMISSION; retry only the identical body with the same submissionId."],
1555
+ "example": {"submissionId": "unique-submission-0001", "configVersion": "from-init", "recordId": "rec_parent", "updatedAt": "from-init", "values": {}, "components": [{"componentId": "cmp_lines", "version": 1, "rows": [{"clientId": "line-1", "operation": "create", "values": {"quantity": 2}}]}]},
1556
+ "rules": "Copy every visible component and its version. Update/delete rows also require recordId and updatedAt. Relation is injected by the server; omit it from row values. API-key receipts are isolated per key. Conflict: reload, reconcile and use a new submissionId. Max 200 resulting detail rows.",
1557
+ },
1558
+ "formula-authoring": {
1559
+ "steps": ["formulas sources OBJECT discovers scalar paths; formulas collections OBJECT discovers stable object/field IDs.", "formulas validate OBJECT --json-file formula.json validates constraints; trial=true uses samples or a saved recordId.", "fields create/update saves metadata. records formula-preview previews runtime values without saving."],
1560
+ "rollupExample": {"fieldType": "rollup", "constraints": {"source": {"mode": "reverse", "objectId": "obj_lines", "relationFieldId": "fld_order", "fieldId": "fld_amount"}, "operation": "sum", "resultType": "number", "scale": 2}, "trial": True, "recordId": "rec_order"},
1561
+ "collectionExample": {"expression": "sum(lines)", "resultType": "number", "scale": 2, "languageVersion": 2, "collections": {"lines": {"mode": "reverse", "objectId": "obj_lines", "relationFieldId": "fld_order", "fieldId": "fld_amount"}}},
1562
+ "readOnly": "formula/rollup values cannot be written, filtered, sorted or SQL-aggregated through the employee CLI. Read results and formulaStates instead.",
1563
+ },
1564
+ "workflow-python": {
1565
+ "discover": "workflows python-runtime OBJECT new (or WORKFLOW) returns creationObjects, relation bindings and limits.",
1566
+ "example": 'import flowbase\n\ndef run():\n first = flowbase.records.create(object="task", values={"title": "Review"})\n others = flowbase.records.create_many(object="task", records=[{"title": "Prepare"}])\n flowbase.records.update(first, values={"title": "Review order"})\n',
1567
+ "atomicity": "Creates and updates commit together after successful execution. Use object/field keys, writable ordinary fields, and returned handles. At most 100 created+updated records.",
1568
+ "retry": "workflows executions OBJECT WORKFLOW lists results; workflows retry OBJECT WORKFLOW EXECUTION --yes retries eligible failed tasks using the original task contract.",
1569
+ },
1383
1570
  "workflow-branches": WORKFLOW_BRANCHES,
1384
1571
  "approval-rules": APPROVAL_RULES,
1385
1572
  "page-buttons": PAGE_BUTTONS,
@@ -1402,6 +1589,7 @@ CATALOGS = {
1402
1589
  },
1403
1590
  "page-components": PAGE_COMPONENTS,
1404
1591
  "page-rules": PAGE_RULES,
1592
+ "forms": FORMS,
1405
1593
  "id-prefixes": RESOURCE_ID_PREFIXES,
1406
1594
  "icons": {
1407
1595
  "note": "6146 Tabler icon names are supported; any Icon* name from the "
@@ -8,7 +8,8 @@ GUIDE = {
8
8
  "flowbase apps list;flowbase objects list:发现已有应用和对象,优先复用,不猜测 ID。",
9
9
  "flowbase objects get <object_id>:读取字段 key、类型、约束和真实选项 ID;关联字段继续读取目标对象。",
10
10
  "flowbase docs catalogs authoring;flowbase docs list;flowbase docs show <endpoint_key>:查请求和更新方式。",
11
- "配置页面前读取 pages options、page-components、page-rules、page-buttons;配置流程前读取 workflow-nodes、workflow-branches 或 approval-rules;配置打印模板前读取 print-templates。",
11
+ "配置页面前读取 pages options、page-components、page-rules、page-buttons;配置流程前读取 workflow-nodes、workflow-branches 或 approval-rules;配置打印模板前读取 print-templates;配置自定义表单前读取 forms。",
12
+ "表单先 forms get,按目录把快照拆成 label、fields、rules、appearance、settings 再 update,baseRevision 用返回的 draftRevision。对象必填且可放入的字段必须保留。set-accepting 与 reset-link 立即生效,不进草稿。",
12
13
  "先 get 再修改,保留未修改的配置。按目录转换更新 envelope;从最近读取或写入返回值获取 draftRevision,作为 baseRevision。",
13
14
  "写后回读验证。records create 返回 recordId,再 records get;values 使用字段 key,选择和关联使用真实 ID。",
14
15
  "遍历列表的所有分页。records list --view-id <view_id> 验证特定视图,records get 获取完整记录。",
@@ -31,6 +32,7 @@ GUIDE = {
31
32
  "docs/guide 离线可读;业务调用需要权限和已启用环境。",
32
33
  "页面规则求值不是浏览器渲染;仪表盘输出验证不是图表视觉验证。打印模板 get 返回源码,管理 CLI 不能生成打印内容;写错的 {{ }} 会保存成功并打印成空白。",
33
34
  "组织候选只读;不能通过管理 CLI 替员工做审批决定。",
35
+ "公开表单的读取、提交和附件上传不是管理命令。logo 与 cover 只能引用已有 fim_ 图片,管理 CLI 不能上传图片。",
34
36
  "删除对象/字段/页面是逻辑删除;没有物理 purge,审计与回收站会保留。",
35
37
  ],
36
38
  }
@@ -522,6 +522,40 @@ def handle_dashboards(args: argparse.Namespace) -> int:
522
522
  )
523
523
 
524
524
 
525
+ def handle_forms(args: argparse.Namespace) -> int:
526
+ action = args.forms_action
527
+ if action == "list":
528
+ return run_request(args, "forms.list")
529
+ if action == "create":
530
+ body = _payload_from_args(
531
+ args,
532
+ {
533
+ "label": args.label,
534
+ "key": args.key,
535
+ "objectId": args.object_id,
536
+ "description": args.description,
537
+ },
538
+ ("label", "key", "objectId"),
539
+ )
540
+ return run_request(args, "forms.create", body=body)
541
+ path_params = {"form_id": args.form_id}
542
+ if action == "get":
543
+ return run_request(args, "forms.get", path_params=path_params)
544
+ if action == "update":
545
+ body = load_json_payload(args.json, args.json_file)
546
+ require_fields(body, "label", "appearance", "settings")
547
+ return run_request(args, "forms.update", path_params=path_params, body=body)
548
+ if action == "copy":
549
+ return run_request(args, "forms.copy", path_params=path_params)
550
+ if action == "share":
551
+ return run_request(args, "forms.share.get", path_params=path_params)
552
+ if action == "set-accepting":
553
+ return run_request(args, "forms.share.set-accepting", path_params=path_params, body={"accepting": args.on})
554
+ if action == "reset-link":
555
+ return run_request(args, "forms.share.reset", path_params=path_params)
556
+ return run_request(args, "forms.delete", path_params=path_params)
557
+
558
+
525
559
  def handle_groups(args: argparse.Namespace) -> int:
526
560
  action = args.groups_action
527
561
  path = {"app_id": args.app_id}
@@ -647,6 +681,10 @@ def handle_workflows(args: argparse.Namespace) -> int:
647
681
  path = {"object_id": args.object_id}
648
682
  if hasattr(args, "workflow_id"):
649
683
  path["workflow_id"] = args.workflow_id
684
+ if action in ("python-runtime", "retry"):
685
+ if action == "retry":
686
+ path["execution_id"] = args.execution_id
687
+ return run_request(args, f"workflows.{action}", path_params=path)
650
688
  if action == "list":
651
689
  return run_request(args, "workflows.list", path_params=path)
652
690
  if action == "get":
@@ -907,8 +945,26 @@ def handle_environments(args: argparse.Namespace) -> int:
907
945
  return 0
908
946
 
909
947
 
948
+ def handle_formulas(args: argparse.Namespace) -> int:
949
+ body = load_json_payload(args.json, args.json_file) if args.formulas_action == "validate" else None
950
+ return run_request(args, f"formulas.{args.formulas_action}", path_params={"object_id": args.object_id}, body=body)
951
+
952
+
910
953
  def handle_records(args: argparse.Namespace) -> int:
911
954
  action = args.records_action
955
+ if action in ("bundle-init", "bundle-status", "bundle-save", "formula-preview", "hierarchy-candidates", "gantt-candidates"):
956
+ path = {"object_id": args.object_id}
957
+ query = {}
958
+ body = None
959
+ if action == "bundle-init":
960
+ query = {"record_id": args.record_id}
961
+ elif action == "bundle-status":
962
+ path["submission_id"] = args.submission_id
963
+ elif action in ("bundle-save", "formula-preview"):
964
+ body = load_json_payload(args.json, args.json_file)
965
+ else:
966
+ query = {"field": args.field, "exclude": args.exclude, "q": args.query}
967
+ return run_request(args, f"records.{action}", path_params=path, query=query, body=body)
912
968
  path = {"object_id": args.object_id}
913
969
  if action == "upload-attachment":
914
970
  body, content_type = multipart_upload(args.file)
@@ -938,7 +994,7 @@ def handle_records(args: argparse.Namespace) -> int:
938
994
  if hasattr(args, "record_id"):
939
995
  path["record_id"] = args.record_id
940
996
  if action == "list":
941
- return run_request(args, "records.list", path_params=path, query={"page": args.page, "viewId": args.view_id})
997
+ return run_request(args, "records.list", path_params=path, query={"page": args.page, "viewId": args.view_id, **({"flat": "false"} if args.view_layout else {}), **{key: value for key, value in {"root": args.root, "parent": args.parent, "locate": args.locate, "hierarchyVersion": args.hierarchy_version}.items() if value is not None}})
942
998
  if action == "get" or action == "delete":
943
999
  return run_request(args, f"records.{action}", path_params=path)
944
1000
  body = load_json_payload(args.json, args.json_file)
@@ -1136,6 +1192,37 @@ def build_parser() -> argparse.ArgumentParser:
1136
1192
  add_yes(dashboard_delete)
1137
1193
  dashboards.set_defaults(handler=handle_dashboards)
1138
1194
 
1195
+ forms = groups.add_parser("forms", help="Manage custom forms (public collection links).")
1196
+ form_actions = forms.add_subparsers(dest="forms_action", required=True)
1197
+ form_actions.add_parser("list")
1198
+ form_get = form_actions.add_parser("get")
1199
+ form_get.add_argument("form_id")
1200
+ form_create = form_actions.add_parser("create")
1201
+ form_create.add_argument("--label")
1202
+ form_create.add_argument("--key")
1203
+ form_create.add_argument("--object-id")
1204
+ form_create.add_argument("--description")
1205
+ add_json_args(form_create, endpoint_key="forms.create")
1206
+ form_update = form_actions.add_parser("update")
1207
+ form_update.add_argument("form_id")
1208
+ add_json_args(form_update, endpoint_key="forms.update")
1209
+ form_copy = form_actions.add_parser("copy")
1210
+ form_copy.add_argument("form_id")
1211
+ form_delete = form_actions.add_parser("delete")
1212
+ form_delete.add_argument("form_id")
1213
+ add_yes(form_delete)
1214
+ form_share = form_actions.add_parser("share", help="Show the public link and collection state.")
1215
+ form_share.add_argument("form_id")
1216
+ form_accepting = form_actions.add_parser("set-accepting", help="Turn public collection on or off (immediate).")
1217
+ form_accepting.add_argument("form_id")
1218
+ accepting_group = form_accepting.add_mutually_exclusive_group(required=True)
1219
+ accepting_group.add_argument("--on", dest="on", action="store_true")
1220
+ accepting_group.add_argument("--off", dest="on", action="store_false")
1221
+ form_reset = form_actions.add_parser("reset-link", help="Reset the public link; the old link stops working.")
1222
+ form_reset.add_argument("form_id")
1223
+ add_yes(form_reset)
1224
+ forms.set_defaults(handler=handle_forms)
1225
+
1139
1226
  app_groups = groups.add_parser("groups", help="Manage app object groups.")
1140
1227
  group_actions = app_groups.add_subparsers(dest="groups_action", required=True)
1141
1228
  group_create = group_actions.add_parser("create")
@@ -1271,6 +1358,14 @@ def build_parser() -> argparse.ArgumentParser:
1271
1358
  workflow_executions.add_argument("object_id")
1272
1359
  workflow_executions.add_argument("workflow_id")
1273
1360
  workflow_executions.add_argument("--result", choices=("success", "failure"))
1361
+ runtime_help = workflow_actions.add_parser("python-runtime")
1362
+ runtime_help.add_argument("object_id")
1363
+ runtime_help.add_argument("workflow_id", help="Use new for a workflow not yet created.")
1364
+ retry = workflow_actions.add_parser("retry")
1365
+ retry.add_argument("object_id")
1366
+ retry.add_argument("workflow_id")
1367
+ retry.add_argument("execution_id")
1368
+ add_yes(retry)
1274
1369
  workflows.set_defaults(handler=handle_workflows)
1275
1370
 
1276
1371
  approvals = groups.add_parser("approvals", help="Manage object approval flow metadata.")
@@ -1408,6 +1503,11 @@ def build_parser() -> argparse.ArgumentParser:
1408
1503
  record_list = record_actions.add_parser("list")
1409
1504
  record_list.add_argument("object_id")
1410
1505
  record_list.add_argument("--page", type=int, default=1)
1506
+ record_list.add_argument("--view-layout", action="store_true", help="Return tree/Gantt projection instead of the default flat list.")
1507
+ record_list.add_argument("--root")
1508
+ record_list.add_argument("--parent")
1509
+ record_list.add_argument("--locate")
1510
+ record_list.add_argument("--hierarchy-version")
1411
1511
  record_list.add_argument("--view-id", help="Select a view returned by views list.")
1412
1512
  record_get = record_actions.add_parser("get")
1413
1513
  record_get.add_argument("object_id")
@@ -1432,6 +1532,19 @@ def build_parser() -> argparse.ArgumentParser:
1432
1532
  record_delete.add_argument("object_id")
1433
1533
  record_delete.add_argument("record_id")
1434
1534
  add_yes(record_delete)
1535
+ for action in ("bundle-init", "bundle-status", "bundle-save", "formula-preview", "hierarchy-candidates", "gantt-candidates"):
1536
+ item = record_actions.add_parser(action)
1537
+ item.add_argument("object_id")
1538
+ if action == "bundle-init":
1539
+ item.add_argument("--record-id")
1540
+ elif action == "bundle-status":
1541
+ item.add_argument("submission_id")
1542
+ elif action in ("bundle-save", "formula-preview"):
1543
+ add_json_args(item, endpoint_key=f"records.{action}")
1544
+ else:
1545
+ item.add_argument("--field", required=True, help="Stable field ID.")
1546
+ item.add_argument("--exclude", required=action == "gantt-candidates", default="")
1547
+ item.add_argument("--query", default="")
1435
1548
  records.set_defaults(handler=handle_records)
1436
1549
 
1437
1550
  roles = groups.add_parser("roles", help="Manage organization roles (production Host only).")
@@ -1451,6 +1564,14 @@ def build_parser() -> argparse.ArgumentParser:
1451
1564
  role_delete.add_argument("role_id")
1452
1565
  add_yes(role_delete)
1453
1566
  roles.set_defaults(handler=handle_roles)
1567
+ formulas = groups.add_parser("formulas", help="Discover, validate and trial formula/rollup fields.")
1568
+ formula_actions = formulas.add_subparsers(dest="formulas_action", required=True)
1569
+ for action in ("sources", "collections", "validate"):
1570
+ item = formula_actions.add_parser(action)
1571
+ item.add_argument("object_id")
1572
+ if action == "validate":
1573
+ add_json_args(item, endpoint_key="formulas.validate")
1574
+ formulas.set_defaults(handler=handle_formulas)
1454
1575
  return parser
1455
1576
 
1456
1577
 
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowbase-cli
3
+ Version: 0.1.15
4
+ Summary: Command line client for the FlowBase OpenAPI
5
+ Author: DeepFlowAI
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # FlowBase CLI
2
11
 
3
12
  `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, approval flows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
@@ -116,6 +125,7 @@ flowbase docs catalogs permissions # function permission points + data sc
116
125
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
117
126
  flowbase docs catalogs page-components # Page component shapes, placement, fields, and enums
118
127
  flowbase docs catalogs page-rules # Page condition operators and action shapes
128
+ flowbase docs catalogs forms # custom form fields, rules, appearance, and settings
119
129
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
120
130
  flowbase docs catalogs icons # icon guidance + examples
121
131
  ```
@@ -270,3 +280,12 @@ flowbase roles update rol_xxx --json-file role.json
270
280
  - `docs catalogs page-buttons`, `workflow-branches`, `approval-rules`, and `dashboard-runtime` describe complex configuration and runtime limits.
271
281
 
272
282
  Transport requests time out after 60 seconds. A lost write response has an unknown outcome: read the resource and current revision before retrying. The CLI never automatically retries writes.
283
+
284
+ ### Recent feature authoring
285
+
286
+ - `flowbase docs catalogs planning`: tree/Gantt view snapshots, candidate queries and optimistic plan updates. Read a full view with `views list` before updating; preserve `displayMode`, `parentFieldId` and `gantt`.
287
+ - `flowbase docs catalogs record-bundles`: initialize, atomically submit and recover a parent/detail submission. Reuse a submission ID only with the identical request body.
288
+ - `flowbase docs catalogs formula-authoring`: formula/rollup sources, validation, saved-record trials and runtime draft preview. Derived values are read-only.
289
+ - `flowbase docs catalogs workflow-python`: record creation examples and failed-task retry. `workflows python-runtime OBJECT new` discovers current object bindings; `workflows executions` includes Python attempts, logs and `canRetry`.
290
+
291
+ Use `records list OBJECT --view-id VIEW --view-layout` for tree/Gantt projections; the default remains a flat list. `records update` accepts `updatedAt` and `planViewId` in its JSON body for version-checked plan edits. Formula validation/trials require both metadata read and record read; workflow retry requires both metadata write and record write and explicit CLI confirmation (`--yes` in automation).
@@ -10,4 +10,5 @@ src/flowbase_cli.egg-info/dependency_links.txt
10
10
  src/flowbase_cli.egg-info/entry_points.txt
11
11
  src/flowbase_cli.egg-info/top_level.txt
12
12
  tests/test_autonomy.py
13
- tests/test_cli.py
13
+ tests/test_cli.py
14
+ tests/test_recent_commands.py
@@ -191,7 +191,7 @@ class FlowBaseCliTest(unittest.TestCase):
191
191
  self.assertEqual(code, 0)
192
192
  field_types = json.loads(stdout.getvalue())
193
193
  type_names = {item["type"] for item in field_types}
194
- self.assertEqual(len(field_types), 25)
194
+ self.assertEqual(len(field_types), 27)
195
195
  self.assertIn("select.single", type_names)
196
196
  self.assertIn("lookup.employee", type_names)
197
197
  self.assertTrue({
@@ -335,6 +335,22 @@ class FlowBaseCliTest(unittest.TestCase):
335
335
  self.assertEqual(prefixes["reg_"], "page region")
336
336
  self.assertEqual(prefixes["cmp_"], "page component")
337
337
  self.assertEqual(prefixes["ptp_"], "print template")
338
+ self.assertEqual(prefixes["frm_"], "custom form")
339
+ self.assertEqual(prefixes["fim_"], "form image")
340
+
341
+ stdout = io.StringIO()
342
+ with patch("sys.stdout", stdout):
343
+ code = main(["docs", "catalogs", "forms"])
344
+ self.assertEqual(code, 0)
345
+ forms = json.loads(stdout.getvalue())
346
+ self.assertEqual(
347
+ forms["field"]["requiredKeys"],
348
+ ["fieldId", "label", "description", "state", "prefill"],
349
+ )
350
+ self.assertEqual(forms["appearance"]["background"], ["gray", "white", "tint"])
351
+ self.assertEqual(forms["settings"]["afterSubmit"], ["message", "redirect"])
352
+ self.assertIn("setFieldState", forms["rule"]["actions"])
353
+ self.assertIn("forms", names)
338
354
 
339
355
  def test_docs_list_marks_body_and_confirmation(self):
340
356
  stdout = io.StringIO()
@@ -0,0 +1,30 @@
1
+ """New discovery commands and retry confirmation remain usable without a browser."""
2
+ import io
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ from flowbase_cli.main import main
8
+
9
+
10
+ @pytest.mark.parametrize('args,endpoint,path,query', [
11
+ (['formulas', 'sources', 'obj_x'], 'formulas.sources', {'object_id': 'obj_x'}, None),
12
+ (['formulas', 'collections', 'obj_x'], 'formulas.collections', {'object_id': 'obj_x'}, None),
13
+ (['records', 'bundle-init', 'obj_x', '--record-id', 'rec_x'], 'records.bundle-init', {'object_id': 'obj_x'}, {'record_id': 'rec_x'}),
14
+ (['records', 'bundle-status', 'obj_x', 'submission'], 'records.bundle-status', {'object_id': 'obj_x', 'submission_id': 'submission'}, {}),
15
+ (['records', 'hierarchy-candidates', 'obj_x', '--field', 'fld_x'], 'records.hierarchy-candidates', {'object_id': 'obj_x'}, {'field': 'fld_x', 'exclude': '', 'q': ''}),
16
+ (['records', 'gantt-candidates', 'obj_x', '--field', 'fld_x', '--exclude', 'rec_x'], 'records.gantt-candidates', {'object_id': 'obj_x'}, {'field': 'fld_x', 'exclude': 'rec_x', 'q': ''}),
17
+ (['workflows', 'python-runtime', 'obj_x', 'new'], 'workflows.python-runtime', {'object_id': 'obj_x', 'workflow_id': 'new'}, None),
18
+ ])
19
+ def test_discovery_request(args, endpoint, path, query):
20
+ with patch('flowbase_cli.main.OpenApiClient.request', return_value=b'{}') as request, patch('sys.stdout', io.StringIO()):
21
+ assert main(args) == 0
22
+ assert request.call_args.args[0] == endpoint
23
+ assert request.call_args.kwargs['path_params'] == path
24
+ assert request.call_args.kwargs['query'] == query
25
+
26
+
27
+ def test_retry_noninteractive_requires_explicit_confirmation():
28
+ with patch('flowbase_cli.main.OpenApiClient.request') as request, patch('sys.stdin.isatty', return_value=False), patch('sys.stderr', io.StringIO()):
29
+ assert main(['workflows', 'retry', 'obj_x', 'wfl_x', 'exe_x']) == 1
30
+ request.assert_not_called()
File without changes