Skip to main content
Version: 2.29

Advanced Blueprint Techniques

This page collects the parts of the Blueprint schema that matter once you are past a first working file. It covers resolving records over a REST API, the idioms for picking a specific row, what Cycle checks before a run, and how to stand up a new system configuration.

Read Building a Blueprint first. Everything here assumes the field vocabulary on that page.

Resolving a record over a REST API

An api op can back an entity-level find or ensure action, not only a field lookup. Cycle issues the request and binds the response to a row, which is how an API-backed entity resolves a record it must not create.

- name: user
connection: jsonApi
action:
find:
api: { method: GET, path: /users/123, select: { user_id: "$.id" } }

A find over an API requires both method and path. How Cycle reads the response depends on its status and shape.

ResponseResult
HTTP 404Not found. For an ensure, the create op runs.
Any other non-2xx statusThe run fails with an error naming the method, path, and status.
2xx with an empty or whitespace-only bodyNot found.
2xx whose top-level JSON is nullNot found.
2xx with a select capture mapEach named field is extracted at its JSONPath.
2xx whose top level is a JSON object, with no selectThe object's scalar fields are bound directly. Non-scalar and null fields are dropped.
2xx whose top level is an array or a scalar, with no selectThe run fails. Add a select to name the fields to bind.

An empty object, written {}, counts as found rather than not found. Values the find returns win over the entity's own declared field values.

Do not use create to resolve an existing record

Cycle does not enforce the HTTP verb on a create op, so a GET written as a create with a select capture does run and does bind its values. The hazard is cleanup: a create entity is removed at teardown, while a find or ensure entity never is. Resolving a record through create therefore points your teardown at data the Blueprint did not create. Use action: find or action: ensure whenever the record must survive.

Picking distinct values across instances

An api field lookup honors exclude_previous only in its array form. Provide a result_path naming the array of candidates plus a plain field name as the scalar select, and Cycle returns the first projected value not already taken.

- name: part
count: 2
connection: partsApi
fields:
- name: code
source:
find:
exclude_previous: true
api: { method: GET, path: /parts/available, result_path: "$.data", select: code }

A bare scalar API lookup has no candidate set to filter, so exclude_previous there would silently return the same value to every instance. Cycle rejects that combination rather than letting it pass quietly.

Each backend applies the exclusion differently, which matters when a pool is large.

BackendHow the exclusion is applied
moca, sqlInjected into the query, so the target system does the filtering
mongodbMerged into the filter as a $nin
flat_fileFiltered after the file is read
apiFiltered from the result_path array after the response arrives

Confirming a per-field pick with an entity find

An entity-level find action re-confirms a value. It cannot itself choose among several candidates, because exclude_previous lives only on a field-level find source.

The idiom is therefore two-part. A field picks the value, and the entity's find action is a narrow probe that re-selects the value already chosen.

- name: customer
action:
find:
sql: { command: "select custnum from customers where wh_id = '{wh_id}' and custnum = '{custnum}'" }
fields:
- name: wh_id
source: { var: { key: wh_id } }
- name: custnum
source:
find:
sql: { command: "select custnum from customers where wh_id = '{wh_id}'", select: custnum }

The field's lookup runs first and binds custnum. The action's probe then confirms that specific row rather than picking a different one.

Checking for conflicts on retry

A retry block's conflict_check runs only against MOCA and SQL connections. The other three backends reject it.

Backendconflict_check
moca, sqlSupported
api, flat_file, mongodbNot supported. The run fails with an error naming the backend

On one of the unsupported backends, you have two options for a unique key. Generate a value collision-resistant enough not to need a retry, or add a verify op that catches a duplicate after the fact.

Validating a Blueprint

Validation happens in two layers. Both run without touching the target system.

Decode-time checks are structural and always on. Each names the slot at fault.

  • An action that is not exactly one of create, find, or ensure.
  • An ensure missing its create or its find.
  • A source that is not exactly one of const, var, find, ask, reuse, or generate.
  • A generate block that is not exactly one of sequence, template, or random.
  • An op naming zero or two backend keys, or carrying an unexpected key beside connection and one backend.
  • A flat_file op carrying both count and validate.

Static checker findings come from blueprint_validate and the --validate-blueprint flag.

FindingCause
Blueprint has no entities definedThe entities list is empty or absent
Entity is missing a nameAn entity declared with no name
Duplicate entity nameTwo entities share a name
Reuse references an unknown entity, or an unknown field on a known entityA reuse source pointing at something that does not exist
Dependency cycle among entitiesTwo entities reuse each other, directly or transitively
Create op has no document to writeA MongoDB create with no document
Op carries a document or body where a read or delete was expectedA create payload on a find, verify, or teardown
Find op should read, not writeA MOCA or SQL find whose command leads with a write verb
Teardown op should deleteA MOCA or SQL teardown whose command leads with a verb that clearly does not delete
expect_rows is only meaningful on a verify opexpect_rows set anywhere else
Select map form is only valid on a create opA capture map on a find
Verify has no filter and would pass triviallyA MongoDB or flat-file verify matching every row
exclude_previous on an API find requires a result_pathThe silently-inert combination described above
Unresolved placeholderA {token} nothing in scope resolves
Malformed components blockA component with no table, no phrases, or a phrase mapped to two different tables

Reuse and cycle findings accumulate rather than stopping at the first one, so a single validation run reports every dependency problem at once.

The find and teardown verb checks read only the first word of a command, case-insensitively, and accept anything they do not recognize. They catch obvious errors rather than every one. A SQL soft delete such as update orders set deleted=1 where ... deliberately validates clean.

An undeclared component is not a static finding. It surfaces at run time as an unknown entity error, because only an offline MOCA run reads the components block.

Scaffolding a new system configuration

A library is plain directories and files, so standing up a new system means creating them.

  1. Create {libraryDir}/{system}/blueprints, {libraryDir}/{system}/lookups, and {libraryDir}/{system}/create-specs.
  2. Write {libraryDir}/{system}/system-config.yml describing the system's connections, default values, and components.
  3. Write {libraryDir}/project.yml recording the library's intended defaults.
default_system_config: blue-yonder-wms
defaults:
connection: wms-moca
credential: wms-moca
vars:
wh_id: WMD1
client_id: CLIENT_A

project.yml is an authoring convention that records intent for whoever reads the library next. Cycle does not read it at run time. Writing it replaces any file already at that path, so check the existing values first.

Never commit a secret to a library file

Do not write a literal password, token, or client secret into system-config.yml, project.yml, or any Blueprint. Use ${ENV_VAR_NAME} instead. Cycle expands it once, at the moment the connection resolves.

Reading a system configuration

A system-config.yml mixes runtime input with authoring notes, and the difference is easy to miss.

BlockRead at run time
connectionsYes
defaultsYes
componentsYes, but only by the offline MOCA emulator
specified_operations, inferred_operations, lookupsNo. These describe the system for a human or an agent deciding what a Blueprint could do

The same split applies to the lookups/ and create-specs/ folders. They are authoring aids, so read them while drafting to match the library's existing conventions, but do not expect Cycle to act on them.

A component maps MOCA entity phrases to the physical table behind them. That mapping is specific to a system and its version, so it is declared rather than built into Cycle.

components:
- table: ord
phrases: [order, orders]
- table: ord_line
phrases: [order line, order lines]
- table: invdtl
phrases: [inventory]

List singular and plural forms explicitly, because create order and list orders are different phrases and Cycle does not pluralize for you. Phrases match case-insensitively. A phrase declared inline in a Blueprint overrides the same phrase from the system configuration, the same way an inline connection overrides one by name. An offline MOCA run resolves tables only from declared components, with no fallback.