Asset Registry & Data Steps
Asset Registry & Data Steps
Section titled “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.
What is an asset?
Section titled “What is an asset?”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).
Workflow step modes at a glance
Section titled “Workflow step modes at a glance”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 |
Reading the whole asset (data_to)
Section titled “Reading the whole asset (data_to)”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_foundon_failure triggers if no asset with that name exists for the company — useful for detecting unregistered assets.
Writing the whole asset (data_from)
Section titled “Writing the whole asset (data_from)”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: approveddata_to and data_from are mutually exclusive — include only one per step.
Reading a single field (field + data_to)
Section titled “Reading a single field (field + data_to)”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_statusUse 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_teamReading multiple fields (fields_to)
Section titled “Reading multiple fields (fields_to)”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_stepPartial merge write (merge_from)
Section titled “Partial merge write (merge_from)”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_teamThis 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_teamChoose 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.
Setting a category on write (category)
Section titled “Setting a category on write (category)”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: doneAll 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_stepasset_name is not required for this mode. The step succeeds with an empty list [] when no assets match — it does not fail.
Bulk upsert from an array (bulk_upsert)
Section titled “Bulk upsert from an array (bulk_upsert)”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_endproperties_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.
Soft-deleting an asset (delete: true)
Section titled “Soft-deleting an asset (delete: true)”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: doneThe step fails gracefully (returns on_failure) if the asset is already deleted or was never created.
Dynamic asset names
Section titled “Dynamic asset names”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 runtimeload_baseline: type: automatic asset: asset_name: "DO-{{droplet_id}}" data_to: baseline on_complete: continue_to: compare_config on_failure: continue_to: unregistered_dropletThe 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”Step 1 — Provision and register
Section titled “Step 1 — Provision and register”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: approvedSee 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}}).
Step 2 — Scheduled drift check
Section titled “Step 2 — Scheduled drift check”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: approvedIterating 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.
Pattern (built-in — recommended)
Section titled “Pattern (built-in — recommended)”- A
list_by_categoryasset step fetches all assets of the given category. - A
field_mappingstep reshapes the list into the form your spawn step expects. - A
spawnstep 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: approvedNo external connector or API token is required — list_by_category queries the asset registry directly within the workflow engine.
Alternative: External REST API
Section titled “Alternative: External REST API”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.
Access control
Section titled “Access control”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.
Version history
Section titled “Version history”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.