Peter Žáčik
06/12/2026, 10:15 AMMarvin
06/12/2026, 10:15 AMMarvin
06/12/2026, 10:18 AMjson
{
"$ref": {
"block_document_id": "..."
}
}
—not the actual credential values.
At runtime, Prefect resolves that reference and loads the block from block storage.
So this is safe relative to putting raw secrets in parameters.
What you should avoid
Do not pass credentials directly as normal flow parameters like:
python
@flow
def my_flow(api_key: str):
...
Those kinds of values are much more likely to be surfaced in run parameters, UI views, logs, or API payloads.
Recommended patterns
1. Best for shared credentials/config: use a saved block
python
from prefect import flow
from prefect.blocks.system import Secret
@flow
def my_flow(secret_block: Secret):
token = secret_block.get()
print("Loaded secret at runtime")
If that block is a saved block document, Prefect can represent it by reference.
2. Also good: pass a block name, then load inside the flow
python
from prefect import flow
from prefect.blocks.system import Secret
@flow
def my_flow(secret_block_name: str):
secret_block = Secret.load(secret_block_name)
token = secret_block.get()
This is often the clearest pattern operationally:
- deployment parameter is just a simple string like "prod-api-token"
- secret material stays entirely in block storage
- changing environments is easy
Which pattern should you choose?
- Use a block parameter/ref if you want the flow signature to explicitly require a block type.
- Use a block name string if you want simpler deployment parameters and easier environment switching.
For many teams, passing the block name and calling .load() inside the flow is the most straightforward pattern.
Does it leak to the UI?
Not the credentials themselves, if you are passing a saved block/reference.
The UI/API may show:
- that a block-backed parameter exists
- a block reference / document ID
- possibly the block/document name depending on how you load/use it
But not the decrypted secret values.
Rule of thumb
- Sensitive reusable config → use Blocks
- Values that vary run-to-run → use parameters
- Never put raw secrets in plain parameters unless you accept that risk
Relevant docs:
- Blocks
- Flow parameters
- Deployments in Python
If you want, I can also show:
- a full deployment example using a block parameter, or
- the recommended pattern in prefect.yaml / prefect deploy.Peter Žáčik
06/12/2026, 10:20 AMprefect.yaml. If passing the block via {{ prefect.... }}, the credentials are resolved at deploy-time and leaked in UIMarvin
06/12/2026, 10:20 AMMarvin
06/12/2026, 10:23 AM{{ prefect.blocks... }} or {{ prefect.variables... }} in most deployment fields inside prefect.yaml, those values are resolved at deploy time and stored on the deployment. That means they can show up in the deployment configuration/UI.
The important exception is pull steps.
Safe rule:
- Secrets in pull: → generally safe; resolved by the worker at runtime
- Secrets in job_variables: or deployment parameters: → *not safe*; resolved at deploy time and persisted
So the recommended pattern in prefect.yaml is:
1. Use block refs only in pull: for things like Git auth
yaml
pull:
- prefect.deployments.steps.git_clone:
repository: <https://github.com/my-org/my-repo.git>
branch: main
credentials: "{{ prefect.blocks.github-credentials.repo-creds }}"
This is the main supported secure use of block templating in prefect.yaml.
2. Do not inject secret-bearing blocks into job_variables:
Avoid this:
yaml
deployments:
- name: my-deployment
entrypoint: flows.py:my_flow
work_pool:
name: my-pool
job_variables:
env:
API_TOKEN: "{{ prefect.blocks.secret.api-token }}"
Why avoid it:
- it resolves during prefect deploy
- the resolved value is stored with the deployment
- it may be visible in deployment config/UI
3. Instead, pass a non-secret identifier and load the block at runtime in code
Example `prefect.yaml`:
yaml
deployments:
- name: my-deployment
entrypoint: flows.py:my_flow
parameters:
secret_block_name: api-token
work_pool:
name: my-pool
And in your flow:
python
from prefect import flow
from prefect.blocks.system import Secret
@flow
def my_flow(secret_block_name: str):
token = Secret.load(secret_block_name).get()
# use token here
This is usually the best pattern for secrets needed by the flow itself:
- deployment stores only api-token
- secret stays in Prefect block storage
- credentials are loaded only at runtime
4. If the worker/infrastructure itself needs secrets, prefer external secret injection
For infrastructure-level auth, use your runtime platform’s secret mechanisms instead of storing resolved secrets in deployment config:
- Kubernetes Secrets
- ECS task secrets
- Docker / CI environment variables
- cloud-native secret managers
Then reference them as normal environment variables in the running infrastructure.
Example idea:
yaml
deployments:
- name: my-deployment
entrypoint: flows.py:my_flow
work_pool:
name: k8s-pool
job_variables:
env:
API_TOKEN: "{{ $API_TOKEN }}"
But note: {{ $API_TOKEN }} is still taken from the environment where you run prefect deploy. So this is only appropriate if you knowingly want that value baked into the deployment. It is not a runtime-secret lookup.
So for true runtime secret handling, prefer:
- platform-native secret injection into the worker/job, or
- loading Prefect blocks inside the flow/task code
Recommended prefect.yaml pattern summary
For code retrieval secrets:
yaml
pull:
- prefect.deployments.steps.git_clone:
repository: <https://github.com/my-org/my-private-repo.git>
credentials: "{{ prefect.blocks.github-credentials.repo-creds }}"
For flow secrets:
yaml
deployments:
- name: my-deployment
entrypoint: flows.py:my_flow
parameters:
db_credentials_block: prod-db-creds
python
from prefect import flow
from prefect_aws.credentials import AwsCredentials
@flow
def my_flow(db_credentials_block: str):
creds = AwsCredentials.load(db_credentials_block)
What I recommend in practice
- Never put {{ prefect.blocks... }} in job_variables if the block contains secrets
- Use pull: block templating only for deployment source retrieval/auth
- For secrets your flow uses, pass the block name as a plain parameter and call .load() in the flowMarvin
06/12/2026, 10:23 AMprefect.yaml
Docs:
- Blocks
- prefect.yaml
- Flow parameters
- Work pools and job configuration
If you want, I can show a full secure example for one of these cases:
- private Git repo auth in pull:
- API key used inside the flow
- Kubernetes worker with runtime-injected secretsPeter Žáčik
06/12/2026, 10:26 AMMarvin
06/12/2026, 10:27 AMMarvin
06/12/2026, 10:27 AMprefect.yaml using {{ prefect.blocks... }} is not the safe/recommended way for secret-bearing blocks, because it resolves at deploy time
So the answer is:
Yes, a flow parameter can be a Block type.
python
from prefect import flow
from prefect.blocks.system import Secret
@flow
def my_flow(secret_block: Secret):
token = secret_block.get()
But:
No, you generally should not populate that parameter in prefect.yaml with {{ prefect.blocks... }} for secrets.
Because in prefect.yaml, deployment parameters are resolved during deployment creation, and the resolved value gets stored on the deployment.
So in practice, the recommended deployment-safe pattern is usually:
- pass the block name (not necessarily “slug” — usually the saved block document name)
- then call .load(name) inside the flow
Example:
yaml
deployments:
- name: my-deployment
entrypoint: flows.py:my_flow
parameters:
secret_block_name: my-api-token
python
from prefect import flow
from prefect.blocks.system import Secret
@flow
def my_flow(secret_block_name: str):
secret_block = Secret.load(secret_block_name)
token = secret_block.get()
A small terminology note:
- block type slug = something like secret, s3-bucket, aws-credentials
- block document name = the saved instance name, like my-api-token or prod-s3
- what you usually pass is the block document name
So the concise answer is:
- Possible in Python/runtime? Yes, you can pass a Block parameter.
- Recommended in prefect.yaml for secret blocks? No.
- Recommended instead? Pass the saved block’s name and load it at runtime.
If you'd like, I can show the exact patterns that are safe for:
- prefect deploy
- ad hoc flow runs via API/UI
- subflows calling flows with block parametersMarvin
06/12/2026, 10:28 AMMarvin
06/12/2026, 10:28 AMparam:?
For example, are you asking about something like:
yaml
parameters:
my_block: ...
and whether ... can be:
yaml
my_block: "{{ prefect.blocks.secret.my-secret }}"
or:
yaml
my_block:
$ref:
block_document_id: "..."
or something else?
Short answer in advance:
- If param: is a prefect.yaml deployment parameter, then anything templated with {{ prefect.blocks... }} will be resolved at deploy time, so not recommended for secret-bearing blocks.
- If you manually pass a raw block $ref object at run creation time via API/Python, that’s a different case and can work without exposing the secret contents.
If you paste the exact YAML/Python snippet, I can tell you whether it’s safe.Peter Žáčik
06/12/2026, 10:29 AMMarvin
06/12/2026, 10:29 AMMarvin
06/12/2026, 10:29 AM$ref, what shows up there is the reference object, e.g.
json
{
"$ref": {
"block_document_id": "..."
}
}
—not the block contents.
So:
- Will the parameter be visible? Yes
- Will the secret values inside the block be visible there? No
- Will the block document ID / reference be visible? Yes
That means $ref is generally safe for credential leakage, but not fully opaque:
- users who can view run parameters can see that a particular block document was referenced
- they won’t see the decrypted secret values just from the parameter display
So if your concern is specifically:
- “Will the actual credentials leak into Run → Parameters?” → No
- “Will the existence/ID of the block reference be visible?” → Yes
Practical recommendation
- If you’re creating runs programmatically and can pass a $ref directly, that is a valid way to pass a block parameter without exposing the block’s contents.
- If you’re defining deployment defaults in prefect.yaml, prefer passing the block document name and loading it in code, because prefect.yaml templating resolves too early.
Best patterns by context
Programmatic run creation:
python
client.create_flow_run(
...,
parameters={
"creds": {
"$ref": {
"block_document_id": "..."
}
}
}
)
Safe from secret-value leakage in parameters.
Deployment defaults in `prefect.yaml`:
yaml
parameters:
creds_block_name: prod-creds
python
@flow
def my_flow(creds_block_name: str):
creds = MyCredentialsBlock.load(creds_block_name)
If you want, I can show you:
- a complete example of passing a block $ref via Python/API, or
- how the UI/API payload differs between $ref and {{ prefect.blocks... }}.Peter Žáčik
06/12/2026, 10:32 AMMarvin
06/12/2026, 10:32 AMMarvin
06/12/2026, 10:32 AMprefect.yaml is for deployment specification, not a general “store this raw object untouched” channel.
A raw $ref in prefect.yaml is not the normal supported authoring pattern, and there are a few reasons it’s not what I’d recommend:
1. prefect.yaml is resolved and processed at deploy time
Prefect treats prefect.yaml as config to be rendered into a deployment definition. That means the file is subject to deploy-time processing, validation, schema generation, and serialization behavior.
So even though a $ref object may look like a stable low-level representation, prefect.yaml is not really documented as “you should hand-author internal block reference objects here”.
2. $ref uses an internal identifier
A block $ref typically contains a block_document_id, which is:
- environment-specific
- not human-friendly
- not portable across workspaces/accounts
- brittle if you recreate the block
Example:
yaml
parameters:
creds:
$ref:
block_document_id: 550e8400-e29b-41d4-a716-446655440000
That deployment now depends on a specific UUID in one workspace. If you move to another workspace or recreate the block, that ID changes.
By contrast, this is much easier to manage:
yaml
parameters:
creds_block_name: prod-creds
then:
python
creds = MyBlock.load("prod-creds")
3. It’s lower-level than the public workflow most users should rely on
$ref is the serialization format Prefect uses to represent block references. It’s useful and safe, but it’s closer to an internal wire/storage representation than the most maintainable config format for humans to write.
In other words:
- supported conceptually? Yes
- best hand-written deployment pattern? Usually no
4. It is still visible in Run → Parameters
Even though secrets do not leak, the $ref object itself is visible in the UI/API. So if your goal is a cleaner operator experience, passing a block name is usually nicer than showing a UUID reference object.
5. Name-based loading is more portable and explicit
Loading by name in code:
- is easier to read
- works cleanly across deployments
- avoids embedding workspace-specific IDs in YAML
- makes intent obvious to other engineers
Example:
yaml
parameters:
aws_credentials_block_name: prod-aws
python
from prefect import flow
from prefect_aws.credentials import AwsCredentials
@flow
def my_flow(aws_credentials_block_name: str):
creds = AwsCredentials.load(aws_credentials_block_name)
That’s usually the most maintainable approach.
So is $ref forbidden in prefect.yaml?
Not exactly — I’m saying it’s not the recommended pattern, mainly because it’s brittle and low-level, not because it would expose the secret contents.
Summary
- $ref in prefect.yaml does not leak the underlying credentials
- but it is not recommended because it embeds internal block document IDs into deployment config
- the preferred pattern is to pass the saved block name and call .load() at runtime
If you want, I can give you a direct yes/no matrix for these three cases:
- prefect.yaml + {{ prefect.blocks... }}
- prefect.yaml + raw $ref
- API/Python run creation + raw $ref
so you can choose the least bad option for your setup.