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.


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.


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.