Skip to main content

Deployment Activities

Sixty-seven activities make up moco's control plane: namespaces, workflowspecs, packages and their files, stages, deployments and targeting, users and groups, roles and privileges, API keys and audit logs.

Most people never call these directly. The moco CLI and the console drive them through the system workflows in moco-core/src/moco/core/workflow/sys_workflow/moco namespace, moco deploy, moco login and the rest. Use them when you are automating the platform itself: a release pipeline that publishes a package and rolls it out, a provisioning workflow that creates a namespace with its roles, a reporting workflow over the audit log.

Because they are one CRUD surface over one database, this page documents them by family rather than one long section per activity: a table covering every activity in the family, then one worked example. The shapes are consistent enough that the tables are the reference and the example shows the idiom.

Setup

No per-call credentials. The provider connects using MOCO_DEPLOY_DB_CONN_STR.

Authorization is enforced by the system workflows that wrap these activities (for example sys.deploy.assert_privilege), not by the activities themselves — so a workflow calling them directly is operating at the control plane's own level of trust.

Conventions across every family

  • *.set is an upsert. It creates the record if absent and updates it if present. This is deliberate: it makes a retried activity safe, where a plain create would fail on a duplicate key. Consequently *.set activities keep the default max_attempts: 3.

  • *.delete is not retried (max_attempts: 1), and neither is apikey.create, which mints new material on every call.

  • *.add / *.remove for memberships and targets are idempotent, so they are safe to retry.

  • Output shapes follow the operation, not the entity:

    OperationReturns
    *.getThe record as an object, or null when it does not exist
    *.listA list of record objects
    *.set, *.activate, *.deactivate, *.addThe resulting record
    *.delete{deleted: true}
    *.remove{removed: true}, or {removed: false, already_removed: true}
  • Timeouts are 60 s everywhere except deploy_package and package.create_with_files, which get 120 s.


Namespaces

A namespace is the top-level grouping for workflowspecs, and the unit access control is scoped to.

ActivityPurposeRequired inputOptional input
builtin.deploy.namespace.setCreate or update a namespace (upsert by name)namespace_id, namespace_namedescription
builtin.deploy.namespace.getFetch one namespacenamespace_id
builtin.deploy.namespace.listList every namespace
builtin.deploy.namespace.get_privilegedList namespaces where the calling user holds any privilege
builtin.deploy.namespace.deleteDelete a namespacenamespace_id

Example — from sys_workflow/sys.deploy.namespace.yaml:

- activity:
type: builtin.deploy.namespace.set
input_data:
namespace_id: '{{namespace_id}}'
namespace_name: '{{namespace_name or namespace_id}}'
description: '{{description}}'
output_name: result
Creating a namespace is more than one activity

The system workflow follows namespace.set with sys.deploy.namespace.authorization to initialize the namespace's roles. A provisioning workflow of your own should do the same, or the namespace exists with nobody able to use it.


Workflowspecs

A wfspec record is the identity of a workflow — its name, namespace and ownership. The YAML itself lives in package files, versioned separately.

ActivityPurposeRequired inputOptional input
builtin.deploy.wfspec.setCreate or update a wfspecwfspec_namenamespace_id, description, owner, tags
builtin.deploy.wfspec.getFetch one wfspecwfspec_name
builtin.deploy.wfspec.listList wfspecs in a namespacenamespace_id
builtin.deploy.wfspec.deleteDelete a wfspecwfspec_name

Example

- activity:
name: register-wfspec
type: builtin.deploy.wfspec.set
input_data:
wfspec_name: "reporting.daily-summary"
namespace_id: "reporting"
description: "Daily summary report"
owner: "{{ owner }}"
tags: ["reporting", "scheduled"]
output_name: wfspec

Packages and files

A package is one semantic version of a wfspec, holding the YAML files that make it up. Versions are version_major.version_minor.version_patch.

ActivityPurposeRequired inputOptional input
builtin.deploy.package.setCreate a package version (idempotent by wfspec + version)wfspec_namepackage_id, version_major, version_minor, version_patch, manifest, created_by
builtin.deploy.package.create_with_filesCreate a package and its files atomicallywfspec_name, filespackage_id, version fields, manifest, created_by
builtin.deploy.package.getFetch a package by id, or by wfspec + versionpackage_id, wfspec_name, version fields
builtin.deploy.package.listList every version of a wfspecwfspec_name
builtin.deploy.package.get_filesList a package's filespackage_id
builtin.deploy.package.deleteDelete a packagepackage_id
builtin.deploy.package.file.setAdd or update one file (idempotent by package + filename)package_id, file_order, filename, contentfile_id, input_schema, output_schema
builtin.deploy.package.file.getFetch one file by namepackage_id, filename
builtin.deploy.package.file.deleteDelete one filefile_id

Example — from sys_workflow/sys.deploy.package.yaml:

- activity:
condition: '{{operator=="set"}}'
type: builtin.deploy.package.set
input_data:
package_id: '{{package_id}}'
wfspec_name: '{{wfspec_name}}'
version_major: '{{version_major}}'
version_minor: '{{version_minor}}'
version_patch: '{{version_patch}}'
output_name: result

Creating a package and its files in one step:

- activity:
name: publish-version
type: builtin.deploy.package.create_with_files
input_data:
wfspec_name: "reporting.daily-summary"
version_major: 1
version_minor: 2
version_patch: 0
created_by: "{{ __user_info__['user_id'] }}"
files:
- filename: "daily-summary.yaml"
file_order: 0
content: "{{ wfspec_yaml }}"
retry_policy:
timeout_sec: 120
output_name: package
Prefer create_with_files

It writes the package and every file in one transaction, so a failure cannot leave a version half-published. Use package.set plus package.file.set only when you are adding files to a package that already exists.


Stages, deployments and targeting

A deployment puts one package into one stage, and targets decide which users or groups see it — the mechanism behind a staged rollout.

ActivityPurposeRequired inputOptional input
builtin.deploy.stage.listList the available stages
builtin.deploy.deployment.setCreate or update a deployment (upsert by id, or by package + stage)package_id, stage_iddeployment_id, is_active, deployed_by
builtin.deploy.deployment.getFetch one deploymentdeployment_id
builtin.deploy.deployment.listList deploymentsstage_id, package_id
builtin.deploy.deployment.activateMake a deployment livedeployment_id
builtin.deploy.deployment.deactivateTake a deployment out of servicedeployment_id
builtin.deploy.target.addTarget a user or groupdeployment_idtarget_user_id, target_group_id, created_by
builtin.deploy.target.listList a deployment's targetsdeployment_id
builtin.deploy.target.removeRemove a targettarget_id, deployment_id, target_user_id, target_group_id
builtin.deploy.deploy_packageCreate the deployment and add its targets in one callpackage_id, stage_idtarget_user_ids, target_group_ids, deployed_by
builtin.deploy.undeploy_packageDeactivate a deploymentdeployment_idperformed_by

Example — from sys_workflow/sys.deploy.deployment.yaml:

- activity:
condition: '{{operator=="set"}}'
type: builtin.deploy.deployment.set
input_data:
deployment_id: '{{deployment_id}}'
package_id: '{{package_id}}'
stage_id: '{{stage_id}}'
deployed_by: '{{deployed_by}}'
output_name: result

- activity:
condition: '{{operator=="activate"}}'
type: builtin.deploy.deployment.activate
input_data:
deployment_id: '{{deployment_id}}'
output_name: result

A staged rollout in one activity:

- activity:
name: roll-out-to-beta
type: builtin.deploy.deploy_package
input_data:
package_id: "{{ package['package_id'] }}"
stage_id: "beta"
target_group_ids: ["beta-testers"]
deployed_by: "{{ __user_info__['user_id'] }}"
retry_policy:
timeout_sec: 120
output_name: deployment

Deployment queries

Read-only questions about what is deployed and who can see it. These back the platform's own version resolution.

ActivityPurposeRequired inputOptional input
builtin.deploy.query.get_latest_packageLatest package version of a wfspecnamespace_id, wfspec_name
builtin.deploy.query.get_wfspec_versionsEvery package version of a wfspecwfspec_name
builtin.deploy.query.get_targeted_wfspec_versionThe version a given user resolves to in a stagewfspec_name, user_id, stage_idversion_filter
builtin.deploy.query.get_user_deploymentsActive deployments a user can see in a stageuser_id, stage_id
builtin.deploy.query.is_user_targetedWhether a deployment targets a user — returns {is_targeted: bool}user_id, deployment_id
builtin.deploy.query.get_group_membersGroup members, resolved recursively — returns {user_ids: [...]}group_id

Example

- activity:
name: resolve-version
type: builtin.deploy.query.get_targeted_wfspec_version
input_data:
wfspec_name: "reporting.daily-summary"
user_id: "{{ target_user }}"
stage_id: "prod"
output_name: resolved # -> the package record, or null

Users

ActivityPurposeRequired inputOptional input
builtin.deploy.user.setCreate or update a user account (upsert; only non-null fields are written)user_iddisplay_name, email, user_org, user_metadata, record_login
builtin.deploy.user.getFetch a useruser_id
builtin.deploy.user.listList usersstatus, limit, offset
builtin.deploy.user.disableDisable an account and revoke its API keys (idempotent)user_id
builtin.deploy.user.deleteDelete an account and its API keysuser_id

Example

- activity:
name: record-login
type: builtin.deploy.user.set
input_data:
user_id: "{{ user_id }}"
display_name: "{{ display_name }}"
email: "{{ email }}"
record_login: true # stamps timestamps and promotes a provisional account
output_name: user
Disabling blocks API keys, not tokens

user.disable revokes the account's API keys, but an already-issued JWT keeps authenticating until it expires.

user.delete refuses system accounts, and does not remove the user's group memberships or role grants — clean those up separately if you are removing someone properly.


Groups

Groups can contain users and other groups; membership resolves recursively.

ActivityPurposeRequired inputOptional input
builtin.deploy.group.setCreate or update a group (upsert by id or name)namegroup_id, description
builtin.deploy.group.getFetch a group by id or namegroup_id, name
builtin.deploy.group.listList every group
builtin.deploy.group.list_membersList a group's direct membersgroup_id
builtin.deploy.group.member.addAdd a user or nested groupgroup_idmember_user_id, member_group_id
builtin.deploy.group.member.removeRemove a membermember_id, group_id, member_user_id, member_group_id
builtin.deploy.group.deleteDelete a groupgroup_id

Example

- activity:
name: create-group
type: builtin.deploy.group.set
input_data:
name: "beta-testers"
description: "Early access cohort"
output_name: group

- activity:
name: add-member
type: builtin.deploy.group.member.add
input_data:
group_id: "{{ group['group_id'] }}"
member_user_id: "{{ user_id }}"
Direct vs recursive membership

group.list_members returns direct members only. For the fully expanded user list, use builtin.deploy.query.get_group_members.


Roles and privileges

Access control is role-based: a resource plus an action makes a privilege, privileges are attached to roles, and users or groups are members of roles.

Resources

ActivityPurposeRequired inputOptional input
builtin.deploy.auth.resource.setCreate or update a resource (upsert by id, or type + value)resource_type, resource_valueresource_id, description
builtin.deploy.auth.resource.getFetch a resource by id, or type + valueresource_id, resource_type, resource_value
builtin.deploy.auth.resource.listList resourcesresource_type
builtin.deploy.auth.resource.deleteDelete a resourceresource_id

Privileges

ActivityPurposeRequired inputOptional input
builtin.deploy.auth.privilege.setCreate or update a privilege (upsert by id, or resource + action)resource_id, actionprivilege_id, description
builtin.deploy.auth.privilege.getFetch a privilegeprivilege_id, resource_id, action
builtin.deploy.auth.privilege.listList privilegesresource_id
builtin.deploy.auth.privilege.deleteDelete a privilegeprivilege_id

Roles

ActivityPurposeRequired inputOptional input
builtin.deploy.auth.role.setCreate or update a role (upsert by id)namespace_id, role_namerole_id, description
builtin.deploy.auth.role.getFetch a rolerole_id
builtin.deploy.auth.role.listList rolesnamespace_id
builtin.deploy.auth.role.deleteDelete a rolerole_id
builtin.deploy.auth.role.privilege.addGrant a privilege to a rolerole_id, privilege_id
builtin.deploy.auth.role.privilege.listList a role's privilegesrole_id
builtin.deploy.auth.role.privilege.removeRevoke a privilege from a rolerole_id, privilege_id
builtin.deploy.auth.role.member.addAdd a user or group to a rolerole_iduser_id, group_id
builtin.deploy.auth.role.member.listList a role's membersrole_id
builtin.deploy.auth.role.member.removeRemove a member from a rolemember_id, role_id, user_id, group_id

Effective access

ActivityPurposeRequired inputOptional input
builtin.deploy.auth.check_user_privilegeWhether a user holds a privilege, directly or via a group — returns {has_privilege: bool}user_id, privilege_id
builtin.deploy.auth.get_user_privilegesEvery privilege a user holds — returns {privileges: [...]}user_idnamespace_id
builtin.deploy.auth.get_user_rolesEvery role a user belongs touser_idnamespace_id

Example — granting a namespace role to a group:

- activity:
name: define-role
type: builtin.deploy.auth.role.set
input_data:
namespace_id: "reporting"
role_name: "reporting-operator"
description: "May run and deploy reporting workflows"
output_name: role

- activity:
name: grant-privilege
type: builtin.deploy.auth.role.privilege.add
input_data:
role_id: "{{ role['role_id'] }}"
privilege_id: "{{ deploy_privilege_id }}"

- activity:
name: add-group-to-role
type: builtin.deploy.auth.role.member.add
input_data:
role_id: "{{ role['role_id'] }}"
group_id: "reporting-team"

Checking effective access:

- activity:
name: may-deploy
type: builtin.deploy.auth.check_user_privilege
input_data:
user_id: "{{ user_id }}"
privilege_id: "{{ deploy_privilege_id }}"
output_name: check # -> {has_privilege: bool}

- abort:
condition: "{{ not check['has_privilege'] }}"
type: raise
message: "{{ user_id }} may not deploy to this namespace"

This is the stored RBAC model. The policy-evaluation activities in Authorization are a different thing: they evaluate a resource policy against evidence about the caller at run time.


API keys

Every activity here operates on the calling user's own keys.

ActivityPurposeRequired inputOptional input
builtin.deploy.apikey.createMint a key. Returns the plaintext oncenamescopes, expires_at
builtin.deploy.apikey.getFetch one key's metadata. Never the secretkey_id
builtin.deploy.apikey.listList the caller's keys. Never the secretsinclude_revoked
builtin.deploy.apikey.revokeRevoke a key (idempotent)key_id
builtin.deploy.apikey.deleteDelete a key permanentlykey_id
builtin.deploy.apikey.lookupLook up verification material by the non-secret key_prefixkey_prefixtouch

apikey.create returns {api_key, key_info}; every other activity returns metadata only.

Example — from sys_workflow/sys.apikey.yaml:

- activity:
condition: '{{operator=="create"}}'
type: builtin.deploy.apikey.create
input_data:
name: '{{name}}'
scopes: '{{scopes}}'
expires_at: '{{expires_at}}'
output_name: result

- activity:
condition: '{{operator=="revoke"}}'
type: builtin.deploy.apikey.revoke
input_data:
key_id: '{{key_id}}'
output_name: result
The plaintext key is returned exactly once

apikey.create is the only place the secret ever exists outside the caller. It is not recoverable afterwards — and it is not retried (max_attempts: 1), because a retry would mint a second key and strand the first. Deliver it to its destination in the same run; do not cache it, and do not write it to workflow state.

apikey.lookup is sensitive

It returns a key's verification material for a caller that will compare the secret itself. It is part of the authentication path, not an administrative convenience.


Audit log

ActivityPurposeRequired inputOptional input
builtin.deploy.audit.get_logsQuery the deployment audit logpackage_id, stage_id, performed_by, action, limit

Example

- activity:
name: recent-deploys
type: builtin.deploy.audit.get_logs
input_data:
stage_id: "prod"
action: "deploy"
limit: 50
output_name: audit_entries