Skip to main content
Version: 13 - TBD

Protecting Child Outputs (secret_outputs)

secret_outputs declares — from a parent flow — which leaves of a child execution's output_value hold sensitive data. The engine wraps those leaves into inline secrets at output finalization, so their cleartext never lands on the child's output_value column, its logs, the live view, records, or git-synced resources.

Use it when a child execution — a connector call, a script, or a sub-flow — returns a value you did not create and cannot wrap from the inside. A REST call that returns an access token, an SSH command that prints a password, a connector whose response embeds a credential: the child produces cleartext in its output, and you name the exact leaf that must be protected before that output is persisted.

Availability

secret_outputs is available from Cloudomation Engine 12. Persisting a wrapped leaf requires the workspace secret store to be unsealed — the same requirement as inline secrets. Until the store is unsealed, serializing a wrapped output raises SecretStoreSealedError.

When to use it

Reach for secret_outputs when the sensitive value originates outside the flow that could wrap it:

  • A connector (REST, SSH, database, …) returns a token, password, or key in its output_value, and you want it persisted encrypted rather than in the clear.
  • A sub-flow or script returns a structure with one or more sensitive leaves, and the parent is the natural place to declare which leaves are secret.

If your own flow is the source of the value — a token you generated, received from an API in memory, or assembled — wrap it directly with system.secret() at the point you produce it. Use secret_outputs for the complementary case: the value already sits in a child's output and you protect it from the parent.

Declaring output secrets

Pass secret_outputs a list of selector paths when you create the child, on this.connect(), this.script(), or this.flow().run():

def handler(system, this, inputs):
result = this.connect(
'rest',
name='fetch access token',
scheme='https',
host='auth.example.com',
path='/oauth/token',
method='POST',
secret_outputs=['report.access_token'],
)

# The token is usable in memory for the next call …
token = result.get('output_value')['report']['access_token'].reveal()
this.connect(
'rest',
name='call downstream API',
scheme='https',
host='api.example.com',
path='/things',
headers=[{'name': 'Authorization', 'value': f'Bearer {token}'}],
)

The engine resolves each selector against the child's output before that output is serialized, and replaces the matched leaf in place with a Secret. On the persisted child execution, report.access_token is stored as an encrypted envelope and shows masked in every display context; read back inside a flow it reconstructs a live Secret you can reveal(). secret_outputs is a reserved field — it is not passed to the connector as an input.

Selector path grammar

A selector names a location inside output_value. The grammar is small and closed on purpose, so a declaration can never wrap more than you intended — there is no recursive descent, no match-everything wildcard, and no operators or functions.

SelectorSelects
reportA top-level key.
report.credentials.passwordA nested object key.
results[0].tokenAn array element by index (negative indices count from the end).
results[*].tokenThe token leaf of every element of results.
headers[name=x-custom-secret].valueThe value of every list element whose name equals x-custom-secret (equality predicate).

Quote a predicate value that contains spaces, ., or ]: headers[name="x-custom-secret"].value.

A selector that lands on a dict or list rather than a scalar wraps that whole subtree as one secret — the engine does not descend into it. A selector using [*] or a predicate that matches several leaves wraps each matched leaf individually.

Fail-closed, and marking a leaf optional

A selector that matches nothing fails the child execution (SecretPathZeroMatchError). This is deliberate: a typo in a path, or a field that a particular run omits, would otherwise leave the author believing a value is protected while the child persisted cleartext — exactly the leak this feature closes. A failing declaration is visible immediately instead of leaking silently.

When a leaf is legitimately optional — present on some runs, absent on others — append a trailing ? to allow zero matches for that path:

this.connect(
'rest',
name='fetch profile',
scheme='https',
host='api.example.com',
path='/me',
secret_outputs=[
'report.email', # required: must be present, or the run fails
'report.backup_token?', # optional: absent on some runs, no error
],
)

Relationship to system.secret

Both features produce the same inline secret — an opaque, encrypted leaf that masks itself everywhere it is serialized. They differ in where the wrapping happens:

system.secret(value)secret_outputs=[...]
Who wrapsThe flow that holds the value wraps it inline.The parent declares which leaves of a child's output to wrap.
Best forA value you generate, receive, or assemble in memory.A value a child execution returns that you cannot wrap from inside.
Where declaredAt the point the value is produced.At child creation (connect() / script() / flow().run()).

Everything downstream of the wrapping is identical: encryption with a per-secret data key wrapped by the workspace master key, masking across logs, output_value, records, and git-sync, and on-demand reveal() in a flow. See Inline Secrets for how the secret store is configured and unsealed, and its threat model.

Behavior reference

AspectBehavior
Where declaredsecret_outputs=[...] on connect(), script(), or flow().run()
Resolves againstThe child execution's output_value, before serialization
EffectMatched leaf/leaves wrapped in place as a Secret
Masked inChild logs, output_value column, live view, records, git-sync
Non-scalar matchThe whole dict / list subtree wrapped as one secret
Zero matchFails the child execution (SecretPathZeroMatchError)
Optional leafAppend ? to the path to allow zero matches
Store sealedSerializing a wrapped output raises SecretStoreSealedError