Skip to main content
Version: 2.29

Building a Blueprint

This page covers writing a Blueprint document: its structure, the field vocabulary, and how to wire related records together. Read Blueprints first for the concepts behind the format. For validating and running one, see Running a Blueprint. For looking up output variables and placeholder rules, see Variables and Placeholders.

Prerequisites

  • A connection and credential for the target system, or an inline connection block in the Blueprint. This is the only hard requirement.
  • Knowledge of the target system's tables, commands, or endpoints. Cycle does not infer them.
  • A blueprint library is optional, and a single self-contained file runs on its own. You need a library once a Blueprint names a system_config, runs by catalog ID, or is shared across a team. A project points at its library through the blueprint.libraryDir setting.
  • If your team already has a library, copying its closest existing Blueprint is faster than starting from a blank file. Look in its {system}/blueprints/ folder first.

One variant per slot

One idea makes the whole format predictable, so it is worth reading before the field tables. At each decision point you pick exactly one option, and you pick it by which key you write rather than by setting a type: value.

An entity carries one action. A field carries one source. Anything that touches the target system carries one backend. Cycle rejects a slot that names none of its options or more than one, so a combination that could not work is impossible to write in the first place.

SlotScopeVariants
actionPer entitycreate, find, ensure
sourcePer fieldconst, var, reuse, generate, ask, find
op kindPer system operationmoca, sql, api, flat_file, mongodb

Because the structure is uniform, the same Blueprint shape works across every supported system. Only the backend key inside each op changes.

Anatomy of a Blueprint

FieldRequiredMeaning
nameYesScenario identifier in kebab-case. Appears in messages only.
descriptionNoDescribes the data state produced, not the mechanism. Note any prerequisites here.
system_configNoIdentifier of a system configuration in the library. Folds in that system's default connection and variables. Values set in the Blueprint always win.
defaultsNoRun-wide connection and vars.
connections / credentialsNoInline connection and credential blocks that make the file self-contained.
componentsNoEntity-to-table mapping read only by the offline MOCA emulator. Inert on a live run.
entitiesYesThe records to set up. Must be non-empty.
template_fileNoA text file rendered after all entities run.
name: create-outbound-order
description: >
One outbound order with one inventory-backed line.

system_config: blue-yonder-wms

defaults:
connection: my-wms
vars:
wh_id: WMD1
client_id: CLIENT_A

connections:
- name: my-wms
type: moca
url: "https://wms.example.com/service"
credentials:
- name: my-wms
username: user
password: "${WMS_PW}"

entities:
- name: order
action:
create:
moca: { command: create order }
fields:
- name: ordnum
source:
generate:
template: { template: "ORD-{seq}", pad: 4 }

An inline connection names its kind directly, and a SQL connection names its vendor rather than a generic SQL type. Each kind uses a different set of keys.

typeKey fieldsNotes
mocaurlThe MOCA service endpoint.
mysql, oracle, sqlserverurlThe three live SQL vendors, named directly. The URL is a JDBC string.
sqliteurlA local file path rather than a JDBC URL, and it needs no credential. An absolute path is recommended. Treat it as an offline datastore rather than a fourth live vendor.
apibase_url, auth_type, token_url, scope, auth_headerThe credential's password doubles as the bearer token or OAuth2 client secret.
mongodburl, database
flat_filebase_dir, formatWrites local files, so it needs no datastore at all. format is csv or xlsx.

A credential pairs to a connection by matching name. Write secrets as ${ENV_VAR_NAME} rather than literal values.

A system configuration uses a different connection vocabulary

In a system-config.yml file, the connection type is one of moca, jdbc, api, mongodb, or sqlite. A single generic jdbc type covers the three live SQL vendors, and Cycle reads the vendor from the jdbc:<vendor>:// URL.

Resolving connections and credentials

Cycle layers connections by name with the precedence inline, then system configuration, then the project's stored connections. A name defined inline in the Blueprint always wins, the system_config context fills in next, and anything still unresolved falls through to the project.

Everywhere a connection is named, it accepts either a bare name or a mapping that also selects a credential.

connection: my-wms # name only, credential pairs by matching name
connection: { name: my-wms, credential: svc } # explicit credential selector

defaults.connection is inherited as a whole unit, meaning the name and credential travel together. An entity that names no connection of its own uses that unit for its field lookups, its action, its verify checks, and its teardown. An entity that names its own connection is left alone.

Name the credential when a profile holds several

A connection pairs with a stored credential by matching name, or automatically when the selected user profile holds exactly one credential. When the profile holds more than one, the connection must name its credential explicitly, either as the mapping form above or through a defaults.credential selector. Otherwise the pairing is ambiguous and the run fails. Resolve it by naming the credential rather than by removing the others.

Generating a file from a template

A top-level template_file block renders a text file after every entity runs, which is how a Blueprint produces an inbound EDI, XML, or JSON file.

template_file:
template: templates/edi-850.edi
output_dir: /data/edi/inbound
file_name: "edi-850-{order.ordnum}.edi"

Cycle substitutes {entity.field} and bare {field} placeholders with realized values and writes the result. The path is published as the variable blueprint-generated-file-path. An existing file is overwritten, and generated files are not removed at teardown.

Template support is partial today. Time-based placeholders, a non-default encoding, and conflict modes are not available, so author around them and remove generated files yourself.

Declaring entities and actions

An entity is one logical record. Names must be unique and non-empty, and snake_case is the convention because names appear in variable keys.

entities:
- name: order
connection: my-wms # optional. Otherwise inherits defaults.connection
group: line # optional. Entity-group membership
count: 1 # optional. Instances to create, default 1
action: # required. Exactly one of create, find, ensure
create:
moca: { command: create order }
fields: [] # optional. Values that feed the action
verify: [] # optional. Checks run after the record is realized
teardown: [] # optional. Cleanup ops, run in reverse order
ActionBehaviorShapeRemoved at teardown
createAlways writes a new record.create: <op>Yes
findResolves an existing record and writes nothing.find: <op>No, because it creates nothing
ensureRuns find first, then create only when the find returns nothing.ensure: { find: <op>, create: <op> }No, because the record may have already existed

An ensure requires both find and create. When the find returns a row, the values it selects overwrite the entity's own field values, so a stored description replaces a literal one. Running an ensure Blueprint twice is the point: the second run must not create a duplicate.

A find action must read rather than write. It cannot carry a create payload such as a MongoDB document or an API body, and its select must be the scalar form.

Cap a find to a single row, written in the connection's own dialect. Use limit 1 for MySQL and SQLite, fetch first 1 rows only or top 1 for Oracle and SQL Server, and a rownum < 2 guard inside bracketed SQL for MOCA. Without a cap, a find that matches several rows is not deterministic.

Choosing a backend op

An op is one system operation. The same shape is used for an action, a field lookup, a verify check, a teardown, and a retry conflict check. Every op names one backend key and may add a sibling connection override.

{ connection: my-db, sql: { command: "..." } }
BackendKey fields
mocacommand for a MOCA command or bracketed SQL, select, expect_rows
sqlcommand for a plain statement, select, expect_rows
apimethod, path, body, select as a JSONPath, result_path pointing at an array, expect_rows. Without a result_path, a 2xx response counts as one row
flat_filefile under the connection's base directory, select, count or validate for checks, filter for a targeted teardown
mongodbcollection, document as a YAML map for a create, filter as a YAML map for a read, select, expect_rows

These are alternatives rather than a list to copy together. Use the one form that matches the entity's connection.

The flat_file op is a simple row and check shape. Column mappings, trigger files, and row-versus-template modes are not available, so use a template_file block when you need rendered output.

moca: { command: "list orders where ordnum = '{ordnum}'", expect_rows: 1 }
sql: { command: "insert into orders (ordnum, qty) values ('{ordnum}', {qty})" }
api: { method: POST, path: /posts, body: '{"title":"{title}"}', select: { post_id: "$.id" } }
flat_file: { file: orders.csv, count: { filter: "ordnum={ordnum}", expect_rows: 1 } }
mongodb: { collection: orders, document: { ordnum: "{ordnum}", whId: "{wh_id}" } }

In a SQL or MOCA command, quote a placeholder that fills a string column and leave a numeric one unquoted. A flat_file filter uses the shape column=value with no spaces around the =. An op's backend must match the kind of the connection it resolves to. A sql op cannot run on a MOCA connection.

Sourcing field values

Fields are acquired from top to bottom, and a later field can reference an earlier one in the same entity as {field_name}.

fields:
- name: ordnum
required: true # default true. Must the field resolve to a value?
allow_override: false # default true. May a caller input replace the source?
source: { ... } # required. Exactly one variant
retry: { ... } # optional. Re-acquire until the conflict check is clean
SourceProducesExample
constA fixed literal.const: { value: "WMD1" }
varA named run value, usually from defaults.vars.var: { key: wh_id }
reuseA value another entity already realized.reuse: { entity: customer, field: custnum }
generateA fabricated value from sequence, template, or random.generate: { template: { template: "ORD-{seq}", pad: 4 } }
askA value the caller must supply. There is no fallback.ask: {}
findA value looked up in the target system.find: { sql: { command: "select prtnum from parts where wh_id = '{wh_id}'", select: prtnum } }

The three generators cover most fabricated values.

generate: { template: { template: "ORD-{seq}", start: 1, pad: 4 } } # ORD-0001, ORD-0002
generate: { sequence: { start: 1, pad: 3 } } # 001, 002
generate: { random: { min: 1, max: 100, prefix: "", suffix: "" } } # a random integer in the range

In all three, start is the first counter value and defaults to 1, and pad is the zero-padded width. A random generator defaults to the range 1 to 100 and can wrap its value in a prefix or suffix.

Always quote a const value that could parse as a number or a boolean. All const values are strings, so write value: "291" rather than value: 291.

Reach for a generator only when the value should change. A sequence used for a value that is meant to stay fixed, such as a sub-line number that is always 1, increments across lines instead. Use const for a fixed value.

Reserve ask for a value the Blueprint genuinely cannot produce, such as an order number created by an earlier step outside its scope. When a sensible default exists, prefer generate and leave allow_override at its default so a caller can still pin a value.

Field modifiers

Four optional modifiers control how a field behaves.

ModifierDefaultMeaning
requiredtrueWhether the field must resolve to a value. A required field whose source produces nothing fails the run. Set it to false for a genuinely optional field.
allow_overridetrueWhether a caller-supplied value may replace the field's source. Set it to false on values the engine must control, such as auto-sequenced line numbers.
exclude_previousfalseOn a field-level find source only. Stops the same value being picked twice across the instances of a count entity or the iterations of a group.
scopePer runOn a sequence or template generator. Every generator declaring the same string shares one counter.

An override supplied for an allow_override: false field is ignored without an error, and the field's own value wins.

When an override does apply, it replaces the field's source outright rather than running it. A generator never fires, so its counter is not spent, and a retry block is skipped. That is why overriding one line's value leaves the other lines' generated numbers unchanged. On an ask source the setting makes no difference, because the value was always going to come from the caller.

A retry block re-acquires a value until a conflict check finds no collision, which matters most for a generated key that must be unique.

- name: ordnum
source:
generate:
template: { template: "ORD-{seq}", pad: 4 }
retry:
max_attempts: 5
conflict_check:
sql: { command: "select ordnum from orders where ordnum = '{ordnum}'" }

The generator produces a candidate, the conflict check runs with that candidate interpolated in, and a match advances the generator and repeats. Exhausting max_attempts fails the run. A conflict check runs only against a MOCA or SQL connection. See Advanced Blueprint Techniques for what to do on the other backends.

Scalar select and capture maps

The select field has two forms with two meanings.

  • Scalar, written select: custnum, binds one value. Use it on a find, whether that is an action or a field source.
  • Capture map, written select: { post_id: "$.id" }, captures several named values from a create response. Each becomes a field on the entity. Use it only on a create op.

A selector is a column name for moca, sql, and flat_file, or a JSONPath string for api and mongodb.

A reuse source is how entities express dependencies. Cycle orders entities by their reuse edges before running any of them, so the referenced entity always runs first. Declaration order is preserved within a dependency tier, which means you can declare entities in the order you think about them.

Three settings control multiple instances. The first two sit on the entity, and scope sits inside a sequence or template generator.

SettingWhere it goesEffect
count: NOn the entityCreates N independent instances of one entity.
group: <name>On the entityMarks entities that iterate together. The group size is the largest count among its members.
scope: <string>Inside a generatorShares one counter across every generator that declares the same string.

A count: N entity already advances its own generator once per instance. Reach for scope only when separate entities must draw from a single shared counter.

Use a group when downstream entities must each reference their own upstream instance from the same iteration, such as three order lines that each need a distinct part and location. Inside a group, a reuse that targets a co-group entity resolves to the current iteration's instance, and a reuse that targets an entity outside the group resolves to the single global instance. Use the plain entity name, because Cycle applies the iteration overlay automatically.

- name: order_line
group: line
count: 2
action:
create:
sql: { command: "insert into order_lines (ordnum, ordlin, prtnum) values ('{ordnum}', '{ordlin}', '{prtnum}')" }
fields:
- name: ordnum
source: { reuse: { entity: order, field: ordnum } } # outside the group, so the one order
- name: ordlin
allow_override: false
source:
generate:
sequence: { start: 1, pad: 3 }
- name: prtnum
source: { reuse: { entity: part, field: prtnum } } # co-group, so this iteration's part

Set exclude_previous: true on a field-level find source when several instances must draw distinct values from the same pool, such as distinct parts or locations. Cycle tracks every value the field has returned and skips the ones already taken. Leave it off for a value that is intentionally shared, such as the order number every line belongs to.

- name: prtnum
source:
find:
exclude_previous: true
sql: { command: "select prtnum from parts where wh_id = '{wh_id}'", select: prtnum }

exclude_previous belongs only on a field-level find source, never on an entity-level find action.

Verifying and tearing down data

A verify list runs immediately after an entity is realized. A failed check stops the run.

verify:
- sql: { command: "select ordnum from orders where ordnum = '{ordnum}'", expect_rows: 1 }

expect_rows is the exact row count the query must return, and it is meaningful only on a verify op. The match is exact rather than a minimum, so a check for one row fails on two rows as well as on zero. That is what catches a duplicate record. A verify that omits expect_rows requires exactly one row.

A teardown list runs at cleanup in reverse order, so the last record created is the first removed. Teardown ops read the previous run's recorded values, which means even a randomly generated order number targets the exact record the run created.

A count or grouped entity runs its teardown once per realized instance, so a per-instance placeholder such as {ordlin} resolves to that instance's own value. Cycle skips a repeat whose interpolated form is identical to one already attempted, so a fixed whole-table or whole-file delete still runs only once. A placeholder still unresolved after interpolation fails the teardown rather than running a delete that would match nothing.

Each line below is the teardown form for one backend, not a list to use together.

teardown:
- sql: { command: "delete from orders where ordnum = '{ordnum}'" }
- moca: { command: "remove order where ordnum = '{ordnum}'" }
- api: { method: DELETE, path: "/posts/{post_id}" }
- mongodb: { collection: orders, filter: { ordnum: "{ordnum}" } }
- flat_file: { file: orders.csv, filter: "ordnum={ordnum}" }

Teardown reports per-op failures through blueprint-teardown-passed and blueprint-teardown-failures rather than failing the Scenario. A SQL soft delete such as update orders set deleted=1 where ... is a supported cleanup pattern. A flat_file teardown with no filter removes the whole file, so add a filter when other runs share that file.

A failed run does not clean up after itself

Cycle does not automatically remove a partially created Blueprint when a stage fails mid-run. Whatever was realized before the failure is still published as output variables, so a following teardown step can target it.