Skip to content

enterprise_apps

enterprise_apps

EnterpriseAppAssets_GET_Error

EnterpriseAppAssets_GET_Error(
    entity_id: str | None = None,
    res: ResponseGetData | None = None,
    message: str | None = None,
    **kwargs
)

Bases: RouteError

Raised when enterprise app asset retrieval operations fail.

This exception is used for failures when downloading app source code, archives, or other asset files.

Source code in src/crew_dcs/routes/enterprise_apps.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def __init__(
    self,
    entity_id: str | None = None,
    res: rgd.ResponseGetData | None = None,
    message: str | None = None,
    **kwargs,
):
    if not message:
        if entity_id:
            message = f"Unable to download assets for enterprise app {entity_id}"
        else:
            message = "Enterprise app asset download failed"

    super().__init__(message=message, entity_id=entity_id, res=res, **kwargs)

EnterpriseApp_CRUD_Error

EnterpriseApp_CRUD_Error(
    operation: str,
    entity_id: str | None = None,
    res: ResponseGetData | None = None,
    message: str | None = None,
    **kwargs
)

Bases: RouteError

Raised when enterprise app create, update, or delete operations fail.

This exception is used for failures during app modification operations, including permission changes and admin assignments.

Source code in src/crew_dcs/routes/enterprise_apps.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def __init__(
    self,
    operation: str,
    entity_id: str | None = None,
    res: rgd.ResponseGetData | None = None,
    message: str | None = None,
    **kwargs,
):
    if not message:
        if entity_id:
            message = f"Enterprise app {operation} failed for app {entity_id}"
        else:
            message = f"Enterprise app {operation} operation failed"

    super().__init__(
        message=message,
        entity_id=entity_id,
        res=res,
        additional_context={"operation": operation},
        **kwargs,
    )

EnterpriseApp_GET_Error

EnterpriseApp_GET_Error(
    entity_id: str | None = None,
    res: ResponseGetData | None = None,
    message: str | None = None,
    **kwargs
)

Bases: RouteError

Raised when enterprise app retrieval operations fail.

This exception is used for failures during GET operations on enterprise apps, including API errors and unexpected response formats.

Source code in src/crew_dcs/routes/enterprise_apps.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def __init__(
    self,
    entity_id: str | None = None,
    res: rgd.ResponseGetData | None = None,
    message: str | None = None,
    **kwargs,
):
    if not message:
        if entity_id:
            message = f"Failed to retrieve enterprise app {entity_id}"
        else:
            message = "Failed to retrieve enterprise apps"

    super().__init__(message=message, entity_id=entity_id, res=res, **kwargs)

add_design_admin async

add_design_admin(
    design_id: str,
    auth: DomoAuth,
    user_ids: list[int],
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Add principals to a design's access list, preserving the existing ones.

PROVEN additive: tests/integration/api_semantics/design_access_semantics_tests.py::test_post_admin_permission_is_additive.

Reads the current access list and re-posts it together with user_ids.

An earlier revision of this docstring called that a defect — claiming it promoted every owner to admin and dropped admins who were not owners. Both claims were investigated live against domo-community on 2026-08-31 and neither reproduces:

  • parts="owners" IS the design's access list. There is no separate readable admin list — GET /designs/{id}/permissions/{LEVEL} is 405.
  • the role on each entry is the principal's INSTANCE role, not a design-level tier (62/62 principals matched their instance roleId), so there is no admin-vs-owner distinction in this payload to get wrong.
  • POST /permissions/ADMIN is ADDITIVE — a narrower POST does not drop prior grantees — so nothing is dropped and nothing is promoted.

The read-modify-write is therefore redundant rather than harmful; it is kept because it also normalises the id types and keeps the call idempotent.

See .agents/guides/access-and-sharing.md.

Source code in src/crew_dcs/routes/enterprise_apps.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def add_design_admin(
    design_id: str,
    auth: DomoAuth,
    user_ids: list[int],
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Add principals to a design's access list, preserving the existing ones.

    PROVEN additive: `tests/integration/api_semantics/design_access_semantics_tests.py::test_post_admin_permission_is_additive`.

    Reads the current access list and re-posts it together with `user_ids`.

    An earlier revision of this docstring called that a defect — claiming it
    promoted every owner to admin and dropped admins who were not owners.
    Both claims were investigated live against domo-community on 2026-08-31 and
    neither reproduces:

      * `parts="owners"` IS the design's access list. There is no separate
        readable admin list — `GET /designs/{id}/permissions/{LEVEL}` is 405.
      * the `role` on each entry is the principal's INSTANCE role, not a
        design-level tier (62/62 principals matched their instance roleId), so
        there is no admin-vs-owner distinction in this payload to get wrong.
      * `POST /permissions/ADMIN` is ADDITIVE — a narrower POST does not drop
        prior grantees — so nothing is dropped and nothing is promoted.

    The read-modify-write is therefore redundant rather than harmful; it is kept
    because it also normalises the id types and keeps the call idempotent.

    See `.agents/guides/access-and-sharing.md`.
    """
    user_ids = user_ids if isinstance(user_ids, list) else [user_ids]

    res = await get_design_owners(design_id=design_id, auth=auth, context=context)

    # Normalise to int and sort: ids arrive as int from the API but callers pass
    # str, and a bare set() of mixed types yields duplicates in a random order.
    merged = {int(owner["id"]) for owner in res.response} | {
        int(user_id) for user_id in user_ids
    }

    return await set_design_admins(
        design_id=design_id, auth=auth, user_ids=sorted(merged), context=context
    )

get_all_designs async

get_all_designs(
    auth: DomoAuth,
    parts: str = "owners,creator,thumbnail,versions,cards",
    debug_loop: bool = False,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> ResponseGetData

Retrieve all enterprise app designs for the authenticated instance.

Fetches a list of all app designs with pagination support. Returns design metadata including owners, creator, thumbnails, versions, and associated cards.

Parameters:

Name Type Description Default
auth DomoAuth

Authentication object containing instance and credentials

required
parts str

Comma-separated list of design parts to include (default: "owners,creator,thumbnail,versions,cards")

'owners,creator,thumbnail,versions,cards'
debug_loop bool

Enable detailed pagination loop logging

False
return_raw bool

Return raw API response without processing

False
context RouteContext | None

Optional RouteContext for request configuration

None
**context_kwargs

Additional context parameters (session, debug_api, etc.)

{}

Returns:

Type Description
ResponseGetData

ResponseGetData object containing list of enterprise app designs

Raises:

Type Description
EnterpriseApp_GET_Error

If app design retrieval fails or API returns an error

Example

designs_response = await get_all_designs(auth) for design in designs_response.response: ... print(f"Design: {design['id']}, Name: {design.get('name')}")

Source code in src/crew_dcs/routes/enterprise_apps.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def get_all_designs(
    auth: DomoAuth,
    parts: str = "owners,creator,thumbnail,versions,cards",
    debug_loop: bool = False,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> rgd.ResponseGetData:
    """
    Retrieve all enterprise app designs for the authenticated instance.

    Fetches a list of all app designs with pagination support. Returns design metadata
    including owners, creator, thumbnails, versions, and associated cards.

    Args:
        auth: Authentication object containing instance and credentials
        parts: Comma-separated list of design parts to include (default: "owners,creator,thumbnail,versions,cards")
        debug_loop: Enable detailed pagination loop logging
        return_raw: Return raw API response without processing
        context: Optional RouteContext for request configuration
        **context_kwargs: Additional context parameters (session, debug_api, etc.)

    Returns:
        ResponseGetData object containing list of enterprise app designs

    Raises:
        EnterpriseApp_GET_Error: If app design retrieval fails or API returns an error

    Example:
        >>> designs_response = await get_all_designs(auth)
        >>> for design in designs_response.response:
        ...     print(f"Design: {design['id']}, Name: {design.get('name')}")
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    url = f"https://{auth.domo_instance}.domo.com/api/apps/v1/designs"

    params = {
        "checkAdminAuthority": True,
        "deleted": False,
        "direction": "desc",
        "parts": parts,
        "search": "",
        "withPermission": "ADMIN",
    }

    offset_params = {
        "limit": "limit",
        "offset": "offset",
    }

    res = await gd.looper(
        url=url,
        method="get",
        fixed_params=params,
        offset_params=offset_params,
        offset_params_in_body=False,
        auth=auth,
        debug_loop=debug_loop,
        timeout=10,
        limit=30,
        return_raw=return_raw,
        arr_fn=lambda x: x.response,
        context=context,
    )

    if return_raw:
        return res

    if not res.is_success:
        raise EnterpriseApp_GET_Error(
            res=res, message="Failed to retrieve enterprise app designs"
        )

    return res

get_design_by_id async

get_design_by_id(
    auth: DomoAuth,
    design_id: str,
    parts: str = "owners,cards,versions,creator",
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> ResponseGetData

Retrieve a specific enterprise app design by its ID.

Fetches details for a single app design identified by its unique ID. Returns design metadata including specified parts like owners, cards, versions, and creator.

Parameters:

Name Type Description Default
auth DomoAuth

Authentication object containing instance and credentials

required
design_id str

Unique identifier for the app design to retrieve

required
parts str

Comma-separated list of design parts to include (default: "owners,cards,versions,creator")

'owners,cards,versions,creator'
return_raw bool

Return raw API response without processing

False
context RouteContext | None

Optional RouteContext for request configuration

None
**context_kwargs

Additional context parameters (session, debug_api, etc.)

{}

Returns:

Type Description
ResponseGetData

ResponseGetData object containing the specific app design data

Raises:

Type Description
EnterpriseApp_GET_Error

If app design retrieval fails

Example

design_response = await get_design_by_id(auth, "8c16c8ab-c068-4110-940b-f738d7146efc") design_data = design_response.response print(f"Design Name: {design_data.get('name')}")

Source code in src/crew_dcs/routes/enterprise_apps.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def get_design_by_id(
    auth: DomoAuth,
    design_id: str,
    parts: str = "owners,cards,versions,creator",
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> rgd.ResponseGetData:
    """
    Retrieve a specific enterprise app design by its ID.

    Fetches details for a single app design identified by its unique ID.
    Returns design metadata including specified parts like owners, cards, versions, and creator.

    Args:
        auth: Authentication object containing instance and credentials
        design_id: Unique identifier for the app design to retrieve
        parts: Comma-separated list of design parts to include (default: "owners,cards,versions,creator")
        return_raw: Return raw API response without processing
        context: Optional RouteContext for request configuration
        **context_kwargs: Additional context parameters (session, debug_api, etc.)

    Returns:
        ResponseGetData object containing the specific app design data

    Raises:
        EnterpriseApp_GET_Error: If app design retrieval fails

    Example:
        >>> design_response = await get_design_by_id(auth, "8c16c8ab-c068-4110-940b-f738d7146efc")
        >>> design_data = design_response.response
        >>> print(f"Design Name: {design_data.get('name')}")
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    url = f"https://{auth.domo_instance}.domo.com/api/apps/v1/designs/{design_id}"

    res = await gd.get_data(
        url=url,
        method="get",
        params={"parts": parts},
        auth=auth,
        context=context,
    )

    if return_raw:
        return res

    if not res.is_success:
        raise EnterpriseApp_GET_Error(entity_id=design_id, res=res)

    return res

get_design_owners async

get_design_owners(
    design_id: str,
    auth: DomoAuth,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Return the principals who have access to a design.

Fetches parts="owners". This IS the design's permission list — there is no separate readable admin list: GET /designs/{id}/permissions/{LEVEL} returns 405 Method Not Allowed (verified on domo-community 2026-08-31). That path is write-only.

⚠️ The role / roleId on each entry is the user's instance role (Admin / Editor / a custom role), NOT their permission on this design. Verified across 62 principals on 30 designs: every entry's roleId equals that user's instance roleId, with zero mismatches. Do not filter this list by role expecting design-level admins — you will be filtering by who happens to be an instance admin.

See .agents/guides/access-and-sharing.md.

Source code in src/crew_dcs/routes/enterprise_apps.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def get_design_owners(
    design_id: str,
    auth: DomoAuth,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Return the principals who have access to a design.

    Fetches `parts="owners"`. This IS the design's permission list — there is no
    separate readable admin list: `GET /designs/{id}/permissions/{LEVEL}` returns
    **405 Method Not Allowed** (verified on domo-community 2026-08-31). That path
    is write-only.

    ⚠️ The `role` / `roleId` on each entry is the user's **instance** role
    (Admin / Editor / a custom role), NOT their permission on this design.
    Verified across 62 principals on 30 designs: every entry's roleId equals that
    user's instance roleId, with zero mismatches. Do not filter this list by
    `role` expecting design-level admins — you will be filtering by who happens
    to be an instance admin.

    See `.agents/guides/access-and-sharing.md`.
    """
    res = await get_design_by_id(
        auth=auth,
        design_id=design_id,
        parts="owners",
        context=context,
    )

    res.response = res.response["owners"]
    return res

get_design_permissions async

get_design_permissions(
    design_id: str,
    auth: DomoAuth,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Deprecated alias for get_design_owners.

The name suggested a permission-level read that this endpoint never performed. Kept for backward compatibility — this is a published library.

Source code in src/crew_dcs/routes/enterprise_apps.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
async def get_design_permissions(
    design_id: str,
    auth: DomoAuth,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Deprecated alias for `get_design_owners`.

    The name suggested a permission-level read that this endpoint never
    performed. Kept for backward compatibility — this is a published library.
    """
    warnings.warn(
        "get_design_permissions is deprecated; use get_design_owners instead. "
        "It fetches parts='owners', which is the design's access list — there is "
        "no separate readable permission list.",
        DeprecationWarning,
        stacklevel=2,
    )
    return await get_design_owners(
        design_id=design_id, auth=auth, context=context, **context_kwargs
    )

get_design_source_code_by_version async

get_design_source_code_by_version(
    auth: DomoAuth,
    design_id: str,
    version: str,
    download_path: str | None = None,
    is_unpack_archive: bool = True,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> ResponseGetData

Download source code for a specific app design version.

Retrieves the source code assets as a ZIP archive for a particular version of an app design. Optionally downloads and unpacks the archive to a local path.

Parameters:

Name Type Description Default
auth DomoAuth

Authentication object containing instance and credentials

required
design_id str

Unique identifier for the app design

required
version str

Version number or identifier to download

required
download_path str | None

Optional local path to save the downloaded archive

None
is_unpack_archive bool

Whether to unpack the downloaded ZIP archive (default: True)

True
return_raw bool

Return raw API response without processing

False
context RouteContext | None

Optional RouteContext for request configuration

None
**context_kwargs

Additional context parameters (session, debug_api, etc.)

{}

Returns:

Type Description
ResponseGetData

ResponseGetData object containing the ZIP archive bytes

Raises:

Type Description
EnterpriseAppAssets_GET_Error

If asset download fails or version not found

Example

asset_response = await get_design_source_code_by_version( ... auth, ... design_id="8c16c8ab-c068-4110-940b-f738d7146efc", ... version="1", ... download_path="/tmp/app_source" ... ) print(f"Downloaded {len(asset_response.response)} bytes")

Source code in src/crew_dcs/routes/enterprise_apps.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def get_design_source_code_by_version(
    auth: DomoAuth,
    design_id: str,
    version: str,
    download_path: str | None = None,
    is_unpack_archive: bool = True,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> rgd.ResponseGetData:
    """
    Download source code for a specific app design version.

    Retrieves the source code assets as a ZIP archive for a particular version
    of an app design. Optionally downloads and unpacks the archive to a local path.

    Args:
        auth: Authentication object containing instance and credentials
        design_id: Unique identifier for the app design
        version: Version number or identifier to download
        download_path: Optional local path to save the downloaded archive
        is_unpack_archive: Whether to unpack the downloaded ZIP archive (default: True)
        return_raw: Return raw API response without processing
        context: Optional RouteContext for request configuration
        **context_kwargs: Additional context parameters (session, debug_api, etc.)

    Returns:
        ResponseGetData object containing the ZIP archive bytes

    Raises:
        EnterpriseAppAssets_GET_Error: If asset download fails or version not found

    Example:
        >>> asset_response = await get_design_source_code_by_version(
        ...     auth,
        ...     design_id="8c16c8ab-c068-4110-940b-f738d7146efc",
        ...     version="1",
        ...     download_path="/tmp/app_source"
        ... )
        >>> print(f"Downloaded {len(asset_response.response)} bytes")
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    url = f"http://{auth.domo_instance}.domo.com/domoapps/designs/{design_id}/versions/{version}/assets"

    res = await gd.get_data_stream(
        url=url,
        method="get",
        auth=auth,
        context=context,
    )

    if return_raw:
        return res

    if not res.is_success:
        if res.response == "Not Found":
            raise EnterpriseAppAssets_GET_Error(
                entity_id=design_id,
                res=res,
                message=f"Assets not found for design {design_id} version {version}",
            )
        raise EnterpriseAppAssets_GET_Error(entity_id=design_id, res=res)

    if download_path:
        archive_path = os.path.join(download_path, "archive.zip")

        dmfi.download_zip(
            output_folder=archive_path,
            zip_bytes_content=res.response,
            is_unpack_archive=False,
        )

        if is_unpack_archive:
            dmfi.download_zip(
                output_folder=download_path,
                zip_bytes_content=res.response,
                is_unpack_archive=True,
            )

    return res

get_design_versions async

get_design_versions(
    auth: DomoAuth,
    design_id: str,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> ResponseGetData

Get all versions for an enterprise app design.

Retrieves the version history for a specific app design, including version numbers, creation dates, and other version metadata.

Parameters:

Name Type Description Default
auth DomoAuth

Authentication object containing instance and credentials

required
design_id str

Unique identifier for the app design

required
return_raw bool

Return raw API response without processing

False
context RouteContext | None

Optional RouteContext for request configuration

None
**context_kwargs

Additional context parameters (session, debug_api, etc.)

{}

Returns:

Type Description
ResponseGetData

ResponseGetData object containing list of design versions

Raises:

Type Description
EnterpriseApp_GET_Error

If version retrieval fails

Example

versions_response = await get_design_versions(auth, design_id="8c16c8ab-c068-4110-940b-f738d7146efc") for version in versions_response.response: ... print(f"Version: {version['version']}, Created: {version.get('createdAt')}")

Source code in src/crew_dcs/routes/enterprise_apps.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def get_design_versions(
    auth: DomoAuth,
    design_id: str,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> rgd.ResponseGetData:
    """
    Get all versions for an enterprise app design.

    Retrieves the version history for a specific app design, including
    version numbers, creation dates, and other version metadata.

    Args:
        auth: Authentication object containing instance and credentials
        design_id: Unique identifier for the app design
        return_raw: Return raw API response without processing
        context: Optional RouteContext for request configuration
        **context_kwargs: Additional context parameters (session, debug_api, etc.)

    Returns:
        ResponseGetData object containing list of design versions

    Raises:
        EnterpriseApp_GET_Error: If version retrieval fails

    Example:
        >>> versions_response = await get_design_versions(auth, design_id="8c16c8ab-c068-4110-940b-f738d7146efc")
        >>> for version in versions_response.response:
        ...     print(f"Version: {version['version']}, Created: {version.get('createdAt')}")
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    url = f"https://{auth.domo_instance}.domo.com/domoapps/designs/{design_id}/versions"

    res = await gd.get_data(
        url=url,
        auth=auth,
        method="get",
        context=context,
    )

    if return_raw:
        return res

    if not res.is_success:
        raise EnterpriseApp_GET_Error(
            entity_id=design_id,
            res=res,
            message=f"Failed to retrieve versions for design {design_id}",
        )

    return res

set_design_admins async

set_design_admins(
    design_id: str,
    auth: DomoAuth,
    user_ids: list[str],
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Grant user_ids access to a design. ADDITIVE, despite the set_ name.

POST /designs/{id}/permissions/ADMIN ADDS the listed principals to the design's access list. Verified on domo-community 2026-08-31: posting [me, A, B] and then posting [me] left A and B in place, so a narrower POST does NOT drop prior grantees. There is no delete-by-omission here and no route in this library removes a design principal.

The set_ prefix is therefore misleading, but it is a published name and the behaviour is the SAFE direction (additive, not destructive), so it is kept rather than churned. Read the list back with get_design_owners if you need to confirm what landed — the response below is built from the INPUT, not from a read-back.

Source code in src/crew_dcs/routes/enterprise_apps.py
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
@gd.route_function
@log_call(
    level_name="route",
    config=LogDecoratorConfig(
        entity_extractor=DomoEntityExtractor(),
        result_processor=DomoEntityResultProcessor(),
    ),
)
async def set_design_admins(
    design_id: str,
    auth: DomoAuth,
    user_ids: list[str],
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Grant `user_ids` access to a design. ADDITIVE, despite the `set_` name.

    `POST /designs/{id}/permissions/ADMIN` ADDS the listed principals to the
    design's access list. Verified on domo-community 2026-08-31: posting
    `[me, A, B]` and then posting `[me]` left A and B in place, so a narrower
    POST does NOT drop prior grantees. There is no delete-by-omission here and
    no route in this library removes a design principal.

    The `set_` prefix is therefore misleading, but it is a published name and
    the behaviour is the SAFE direction (additive, not destructive), so it is
    kept rather than churned. Read the list back with `get_design_owners` if you
    need to confirm what landed — the response below is built from the INPUT,
    not from a read-back.
    """
    url = f"https://{auth.domo_instance}.domo.com/api/apps/v1/designs/{design_id}/permissions/ADMIN"

    res = await gd.get_data(
        url=url,
        method="POST",
        auth=auth,
        body=user_ids,
        context=context,
    )

    if return_raw:
        return res

    if not res.is_success:
        raise EnterpriseApp_CRUD_Error(res=res)

    # NOTE: asserted from the HTTP status and the INPUT — the admin list is never
    # read back. See .agents/guides/access-and-sharing.md.
    res.response = f"successfully set design_id {design_id} admins to {user_ids}"

    return res