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.
| Response | Result |
|---|---|
| HTTP 404 | Not found. For an ensure, the create op runs. |
| Any other non-2xx status | The run fails with an error naming the method, path, and status. |
| 2xx with an empty or whitespace-only body | Not found. |
2xx whose top-level JSON is null | Not found. |
2xx with a select capture map | Each named field is extracted at its JSONPath. |
2xx whose top level is a JSON object, with no select | The 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 select | The 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.
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.
| Backend | How the exclusion is applied |
|---|---|
moca, sql | Injected into the query, so the target system does the filtering |
mongodb | Merged into the filter as a $nin |
flat_file | Filtered after the file is read |
api | Filtered 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.
| Backend | conflict_check |
|---|---|
moca, sql | Supported |
api, flat_file, mongodb | Not 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
actionthat is not exactly one ofcreate,find, orensure. - An
ensuremissing itscreateor itsfind. - A
sourcethat is not exactly one ofconst,var,find,ask,reuse, orgenerate. - A
generateblock that is not exactly one ofsequence,template, orrandom. - An op naming zero or two backend keys, or carrying an unexpected key beside
connectionand one backend. - A
flat_fileop carrying bothcountandvalidate.
Static checker findings come from blueprint_validate and the --validate-blueprint flag.
| Finding | Cause |
|---|---|
| Blueprint has no entities defined | The entities list is empty or absent |
| Entity is missing a name | An entity declared with no name |
| Duplicate entity name | Two entities share a name |
| Reuse references an unknown entity, or an unknown field on a known entity | A reuse source pointing at something that does not exist |
| Dependency cycle among entities | Two entities reuse each other, directly or transitively |
| Create op has no document to write | A MongoDB create with no document |
| Op carries a document or body where a read or delete was expected | A create payload on a find, verify, or teardown |
| Find op should read, not write | A MOCA or SQL find whose command leads with a write verb |
| Teardown op should delete | A MOCA or SQL teardown whose command leads with a verb that clearly does not delete |
expect_rows is only meaningful on a verify op | expect_rows set anywhere else |
| Select map form is only valid on a create op | A capture map on a find |
| Verify has no filter and would pass trivially | A MongoDB or flat-file verify matching every row |
exclude_previous on an API find requires a result_path | The silently-inert combination described above |
| Unresolved placeholder | A {token} nothing in scope resolves |
| Malformed components block | A 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.
- Create
{libraryDir}/{system}/blueprints,{libraryDir}/{system}/lookups, and{libraryDir}/{system}/create-specs. - Write
{libraryDir}/{system}/system-config.ymldescribing the system's connections, default values, and components. - Write
{libraryDir}/project.ymlrecording 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.
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.
| Block | Read at run time |
|---|---|
connections | Yes |
defaults | Yes |
components | Yes, but only by the offline MOCA emulator |
specified_operations, inferred_operations, lookups | No. 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.