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.
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.
| Selector | Selects |
|---|---|
report | A top-level key. |
report.credentials.password | A nested object key. |
results[0].token | An array element by index (negative indices count from the end). |
results[*].token | The token leaf of every element of results. |
headers[name=x-custom-secret].value | The 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 wraps | The flow that holds the value wraps it inline. | The parent declares which leaves of a child's output to wrap. |
| Best for | A value you generate, receive, or assemble in memory. | A value a child execution returns that you cannot wrap from inside. |
| Where declared | At 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
| Aspect | Behavior |
|---|---|
| Where declared | secret_outputs=[...] on connect(), script(), or flow().run() |
| Resolves against | The child execution's output_value, before serialization |
| Effect | Matched leaf/leaves wrapped in place as a Secret |
| Masked in | Child logs, output_value column, live view, records, git-sync |
| Non-scalar match | The whole dict / list subtree wrapped as one secret |
| Zero match | Fails the child execution (SecretPathZeroMatchError) |
| Optional leaf | Append ? to the path to allow zero matches |
| Store sealed | Serializing a wrapped output raises SecretStoreSealedError |
Related
- Inline Secrets (system.secret) — wrap a value your own flow holds.
- Secret Handling with Vault and Devolutions — retrieve secrets from an external secret manager.
- Flow API reference — the
secret_outputsfield onconnect(),script(), andflow().run().