Skip to content

Asset Registry & Data Steps

The asset registry is a key-value store for named configuration records — server baselines, environment snapshots, API profiles, or any JSON data your workflows need to persist between runs. Workflows read and write assets using the asset: step; external scripts enumerate them via the REST API.

Each asset has:

Field Description
name Stable identifier used to look up the record (e.g. DO-87654321)
category Grouping label for filtering (e.g. DigitalOcean, AWS, GCP)
properties Arbitrary JSON — the actual data payload
view_roles Company roles that can read the asset; empty means visible to all
owner_id The employee responsible for the asset (defaults to creator); owner always has read + write access
schema_id Optional schema that defines typed fields — drives the edit UI
updated_at Timestamp of the last write, updated automatically on every upsert

Assets are company-scoped and maintain a full changelog for audit purposes (see version history).


The asset: step supports ten modes, selected by the keys you include.

Single-asset modes (require asset_name):

Keys Direction Scope
data_to asset → variable Whole properties blob
data_from variable → asset Full replace of properties
field + data_to asset → variable Single field value
field + data_from variable → asset Single-field patch (other fields untouched)
fields_to asset → variables Multiple specific fields → separate variables
merge_from variable dict → asset Partial merge, other fields preserved
fields_from variables → asset Multiple variables → separate named fields
delete: true Soft-delete (sets deleted_at; audit history preserved)

Collection modes (no asset_name needed):

Keys Direction Scope
list_by_category + save_to assets → variable All non-deleted assets in a category
bulk_upsert variable array → assets Create or update N assets from an array in one step

Use asset: data_to to load an asset’s entire properties JSON into a workflow variable. The variable is then available to all subsequent steps.

load_baseline:
type: automatic
asset:
asset_name: gcp-iam-baseline
data_to: baseline_data # workflow variable name to populate
on_complete:
continue_to: compare_step
on_failure:
continue_to: no_baseline_found

on_failure triggers if no asset with that name exists for the company — useful for detecting unregistered assets.


Use asset: data_from to upsert a workflow variable back into the asset registry. If the asset doesn’t exist it is created; if it does, properties is fully replaced and updated_at is refreshed.

update_baseline:
type: automatic
asset:
asset_name: gcp-iam-baseline
data_from: current_config # workflow variable to persist
on_complete:
end: approved

data_to and data_from are mutually exclusive — include only one per step.


Add field: to scope the read to one key inside properties. The data_to variable name defaults to the field name if omitted.

read_status:
type: automatic
asset:
asset_name: "ccp-{{product_id}}"
field: current_status # reads properties.current_status only
data_to: current_status # optional; defaults to field name
on_complete:
continue_to: route_on_status

Use this to pull one control-point value into a routing condition without loading the full asset blob.


Patching a single field (field + data_from)

Section titled “Patching a single field (field + data_from)”

Add field: with data_from to update one key inside properties without touching the rest. Other fields in the asset are preserved exactly as-is.

update_status:
type: automatic
asset:
asset_name: "ccp-{{product_id}}"
field: current_status
data_from: new_status # request_data.new_status → properties.current_status only
on_complete:
continue_to: notify_team

Use fields_to: to extract several specific fields into separate named variables in one step.

load_fields:
type: automatic
asset:
asset_name: "ccp-{{product_id}}"
fields_to:
current_status: status_var # properties.current_status → request_data.status_var
last_verified_at: verified_date # properties.last_verified_at → request_data.verified_date
on_complete:
continue_to: compare_step

Use merge_from: to shallow-merge a partial dict into properties. Only the keys present in the source dict are updated; all other fields in the asset are left unchanged.

partial_update:
type: automatic
asset:
asset_name: "ccp-{{product_id}}"
merge_from: update_payload
# request_data.update_payload = {"current_status": "in_control", "last_verified_at": "2025-06-18"}
# Other keys in properties (e.g. critical_limit, monitoring_method) are preserved.
on_complete:
continue_to: notify_team

This is the recommended mode when a workflow step only “owns” a subset of an asset’s fields — for example, an approval step that stamps current_status and last_verified_at without overwriting policy_document or other fields set by different workflows.


Writing multiple fields at once (fields_from)

Section titled “Writing multiple fields at once (fields_from)”

Use fields_from: to write several workflow variables into separate named fields in one step. Each entry maps an asset field name to the workflow variable that should populate it. Fields not listed are left unchanged — this is the write-side counterpart of fields_to.

save_supplier_data:
type: automatic
asset:
asset_name: "supplier-{{supplier_id}}"
fields_from:
status: supplier_status # request_data.supplier_status → properties.status
last_audit_date: audit_date # request_data.audit_date → properties.last_audit_date
approved_by: current_user_email # request_data.current_user_email → properties.approved_by
on_complete:
continue_to: notify_team

Choose fields_from over merge_from when your values come from separate named variables (form fields, earlier step outputs). Choose merge_from when your values are already collected into a single dict variable.


When creating or updating an asset, use category: to assign it a grouping label. This controls which assets list_by_category returns and how assets appear in Admin → Assets. If omitted, the category defaults to 'data'.

register_product:
type: automatic
asset:
asset_name: "product-{{odoo_product_id}}"
category: product
fields_from:
odoo_product_id: odoo_product_id
name: product_name
code: product_code
on_complete:
continue_to: done

All single-asset write modes (data_from, fields_from, merge_from, field+data_from) accept category:. The value is stored in assets.category and can be used to group assets by their domain — for example product, supplier, gcp_project, or do_droplet.

Assigning an asset schema (schema / schema_definition)

Section titled “Assigning an asset schema (schema / schema_definition)”

Write modes also accept optional schema: (schema name) and schema_definition: (inline field definitions). When schema: is set, the engine assigns that schema’s schema_id on create. On update it only fills a NULL schema_id — an existing schema is never overwritten.

If the named schema does not yet exist for the company and schema_definition.fields is provided, the engine creates the schema first:

save_asset:
type: asset
asset_name: "{{resolved_asset_name}}"
category: server
schema: server
schema_definition:
description: "Universal server/compute asset schema"
fields:
provider:
type: text
label: Provider
region:
type: text
label: Region
fields_from:
provider: provider
region: region
on_complete:
end: approved

schema_definition without schema: is ignored (validator warning).

Write modes also set owner_id from the workflow instance’s requestor_id on create (and backfill NULL on update). For cron triggers, that requestor is the employee resolved from requestor_company_role.


Listing all assets by category (list_by_category)

Section titled “Listing all assets by category (list_by_category)”

Use list_by_category to fetch every non-deleted asset in a given category into a workflow variable. The result is an array of {name, properties} objects.

fetch_all_product_assets:
type: automatic
asset:
list_by_category: product # category to filter on
save_to: all_assets # variable receives [{name, properties}, ...]
on_complete:
continue_to: next_step

asset_name is not required for this mode. The step succeeds with an empty list [] when no assets match — it does not fail.


Use bulk_upsert to create or update many assets in a single step from an array variable — ideal for initial seeding from an ERP. Each item is processed with ON CONFLICT DO UPDATE, so the step is safe to re-run.

seed_assets:
type: automatic
asset:
bulk_upsert:
items_from: all_odoo_products # variable containing the source array
asset_name_template: "product-{{id}}" # {{field}} interpolated from each item
category: product
properties_mapping: # {stored_property: source_field}
odoo_product_id: id
name: name
code: default_code
price: list_price
on_complete:
continue_to: done_end

properties_mapping maps {target_key: source_field} — only mapped fields are stored. Item fields not listed are ignored, preventing accidental storage of large or sensitive source data.


Use delete: true to retire an asset. The record is not physically removed — deleted_at is set and it remains visible in the audit history and changelog. It will no longer appear in list_by_category results or read steps.

retire_asset:
type: automatic
asset:
asset_name: "product-{{odoo_product_id}}"
delete: true
on_complete:
continue_to: done

The step fails gracefully (returns on_failure) if the asset is already deleted or was never created.


asset_name supports {{field_name}} interpolation. The placeholder is resolved at runtime from the workflow’s form data, making it straightforward to manage per-item assets.

# asset_name resolves to e.g. "DO-87654321" at runtime
load_baseline:
type: automatic
asset:
asset_name: "DO-{{droplet_id}}"
data_to: baseline
on_complete:
continue_to: compare_config
on_failure:
continue_to: unregistered_droplet

The same interpolation applies to compare_to_asset in data_processor steps.


Full example: provisioning and drift detection

Section titled “Full example: provisioning and drift detection”

A provisioning workflow creates an asset and registers it as a baseline asset.

extract_id:
type: automatic
field_mapping:
new_droplet_id:
jsonata: "$string(created_droplet.droplet.id)"
register_asset:
type: automatic
asset:
asset_name: "DO-{{new_droplet_id}}"
data_from: new_droplet_data
on_complete:
end: approved

See docs/examples/do-droplet-provisioning.yaml for the full flow. Add source_id or source_name on provision_server after creating the Create Droplet data source (Body Template {{payload}}).

A daily scheduled workflow loads each registered baseline and compares it against the live state.

load_baseline:
type: automatic
asset:
asset_name: "DO-{{droplet_id}}"
data_to: baseline
on_complete:
continue_to: compare_config
on_failure:
continue_to: unregistered_droplet # asset was never registered
compare_config:
type: automatic
field_mapping:
drift_detected:
jsonata: |
$string(
current_config.size_slug != baseline.size_slug or
current_config.region_slug != baseline.region_slug
)
on_complete:
continue_to: route_on_drift
route_on_drift:
type: conditional_split
choices:
- conditions: "drift_detected == 'true'"
continue_to: it_review
- conditions: "drift_detected == 'false'"
continue_to: end_clean
it_review:
type: decision
label: "Drift detected — approve updated baseline?"
approver: it_manager
sla: "4h"
on_approve:
continue_to: update_baseline
on_reject:
continue_to: flag_non_compliant
update_baseline:
type: automatic
asset:
asset_name: "DO-{{droplet_id}}"
data_from: current_config # accepted drift becomes new baseline
on_complete:
end: approved

Iterating over registered assets (scheduled workflows)

Section titled “Iterating over registered assets (scheduled workflows)”

When a scheduled workflow needs to run once per registered asset, use list_by_category to build the iteration list, then drive a spawn step with the results.

  1. A list_by_category asset step fetches all assets of the given category.
  2. A field_mapping step reshapes the list into the form your spawn step expects.
  3. A spawn step creates one child workflow per item.
fetch_all_assets:
type: automatic
asset:
list_by_category: do_droplet # fetches [{name, properties}, ...]
save_to: all_assets
on_complete:
continue_to: normalize_assets
normalize_assets:
type: automatic
field_mapping:
asset_list:
jsonata: |
$map(vars.all_assets, function($a) {
{"asset_name": $a.name, "droplet_id": $a.properties.droplet_id}
})
on_complete:
continue_to: check_each_asset
check_each_asset:
type: spawn
workflow: "DO Drift Check"
items: asset_list
wait_for: none
map:
asset_name: asset_name
droplet_id: droplet_id
on_complete:
end: approved

No external connector or API token is required — list_by_category queries the asset registry directly within the workflow engine.

If you need to filter by name_prefix or access assets from an external script, the Aptiwise Assets REST API is also available.

In Admin → Connectors, create a REST API connector:

Field Value
Name Aptiwise Assets API
Type rest_api
Base URL https://your-domain/services/v1/api-tokens/external
Auth type Bearer token
Token ffat_... (a dedicated service account token)

Then create a data source:

Field Value
Name Aptiwise Assets API
Connector Aptiwise Assets API
Method GET
Endpoint /assets

Pass category and name_prefix as query parameters via params: in the data_processor step.


view_roles on each asset determines who can read it via the external API. Set this in Admin → Assets when registering a record.

view_roles value Who can read
[] (empty) All authenticated token holders in the company
["it_admin"] Only employees with the it_admin role
["it_admin", "security"] Employees with either role

Access is checked server-side on every API call — there is no client-side caching of role membership.


Linking assets to an employee (Employee Detail panels)

Section titled “Linking assets to an employee (Employee Detail panels)”

An asset schema can declare that its assets relate to an employee, so they show up automatically as a related-records panel on that employee’s Employee Detail page — without any workflow YAML involved. This generalizes the app_access pattern (one asset per app/user grant) to any schema.

Configure it on the schema itself, in Admin → Assets → Asset Types:

  1. Open (or create) the schema — e.g. App User Access.
  2. Set Link to entity to Employee (the only supported entity today).
  3. Set Matching field to whichever of the schema’s own fields holds the value that identifies the employee — typically email. This dropdown only offers real field keys already defined on the schema.
  4. Save.

Every employee’s Detail page (reached via the eye-icon View button on the Employees list) then shows one panel per linked schema, listing only the assets whose matching field equals that employee’s own value for the same field name — e.g. assets where properties.email == employee.email.

This is read-only from the Employee Detail page — creating, editing, or revoking the underlying assets still happens the normal way, in Admin → Assets or via workflow asset: steps.


Every successful single-asset write (data_from, fields_from, merge_from, field+data_from, delete: true) records an entry in asset_changelog. bulk_upsert does not write per-item changelog entries — it uses a batch SQL upsert for efficiency. You can view the full timeline in Admin → Assets by opening the asset detail panel. Each entry shows:

  • What changed (DeepDiff summary)
  • Which workflow instance triggered the write
  • The timestamp and the requestor

This changelog is append-only and cannot be edited or deleted through the UI.


Server category assets support active user permissions governance via an optional user_access array:

"user_access": [
{
"role": "roles/viewer",
"member": "user:alice@example.com",
"granted_at": "2026-08-10T09:15:00Z",
"granted_by": "admin@company.com"
}
]

These permissions are provisioned dynamically via GCP IAM Access Request and GCP IAM Access Revoke workflows, and verified during scheduled runs of the Universal Asset Drift Check workflow to prevent manual cloud drift.