Jeff Rose
05/04/2026, 4:09 PMMarvin
05/04/2026, 4:09 PMMarvin
05/04/2026, 4:09 PMprefect.deployments.steps.set_working_directory(directory=...) uses Python's os.chdir(directory), so you can pass either:
- a relative path
- an absolute path
A couple of important details:
- Relative paths are resolved relative to the current working directory at runtime
- The step returns the absolute resolved directory
- The directory must already exist, or it will error
So this is valid:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: ./my-subdir
and so is this:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: /opt/prefect/my-subdir
If you want, I can also show how this behaves inside prefect.yaml pull steps specifically, since that’s usually where path confusion comes up.Jeff Rose
05/04/2026, 4:34 PMJeff Rose
05/04/2026, 4:34 PMMarvin
05/04/2026, 4:34 PMMarvin
05/04/2026, 4:34 PMprefect.yaml, I can help you sanity-check a specific example.Jeff Rose
05/04/2026, 4:43 PM- prefect.deployments.steps.set_working_directory:
directory: "."
definitions:
work_pools:
rn-prefect-worker-work-pool: &rn-prefect-worker-work-pool
name: "rn-etl-services-work-pool-{{$env}}"
work_queue_name: "{{$env}}"
job_variables:
image: "<http://384144335252.dkr.ecr.us-east-2.amazonaws.com/{{$service}}-{{$env}}:latest|384144335252.dkr.ecr.us-east-2.amazonaws.com/{{$service}}-{{$env}}:latest>"
task_role_arn: "arn:aws:iam::384144335252:role/{{$service}}-{{$env}}-task-role"
execution_role_arn: "arn:aws:iam::384144335252:role/{{$service}}-{{$env}}-task-exec-role"
name: "{{$service}}-prefect-task-run-{{$env}}"
deployments:
- name: "vacuum_cleaner_{{$env}}"
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
work_pool:
name: "rn-etl-services-work-pool-{{$env}}"
version:
tags:
- "{{$env}}"
- "{{$service}}"
- "vacuum_cleaner"
- "redshift"
description: "Redshift maintenance task - runs vacuum and analyze statements against dw tables."
schedules:
- cron: "0 0 * * *"
timezone: "America/New_York"Marvin
05/04/2026, 4:43 PMMarvin
05/04/2026, 4:43 PMdirectory: "." means:
- "change into the *current working directory*"
- so it effectively does nothing useful unless the current working directory is already exactly what you want
That’s usually not a good idea in deployment pull steps, because the worker’s starting directory may not be stable or obvious.
A few important points for your example:
1. entrypoint is resolved relative to the working directory used at runtime
So with:
yaml
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
Prefect will need the runtime working directory to be the repo/project root that contains rn/....
2. set_working_directory: "." does not guarantee that
It just preserves whatever directory the process already started in.
3. Better options
If your code is already inside the image at a known path, use an absolute path like:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/opt/prefect"
or whatever directory in the container actually contains rn/.
Then this entrypoint works predictably:
yaml
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
4. If you are cloning/pulling code first
Then set the working directory to the known checkout location, or use steps that place code in a specific directory before resolving the entrypoint.
So the short answer is:
- . is allowed
- but in your config it is probably not reliable
- prefer an absolute directory inside the container that contains the rn folder
If you want, I can help you map this to your ECS image layout and suggest the exact directory value to use.Jeff Rose
05/04/2026, 4:45 PMMarvin
05/04/2026, 4:45 PMMarvin
05/04/2026, 4:47 PMprefect deploy.
What’s happening
There are really two different environments:
- the environment where you run prefect deploy
- the environment where the worker executes the flow run
Your entrypoint has to make sense in the execution environment, not just the deploy environment.
If those two images have different working dirs, then relative file-based paths can break unless you normalize them.
Best ways to work around it
Option 1: Put code in a fixed path in the execution image and set that as the runtime working directory
This is the most straightforward approach for Docker/ECS.
For example, if your image contains your repo at /app, then use:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/app"
and keep your entrypoint relative to that:
yaml
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
This makes runtime deterministic, regardless of where prefect deploy was run from.
Option 2: Make your image WORKDIR match where the code lives
If your Dockerfile says:
dockerfile
WORKDIR /app
COPY . /app
then Prefect runtime behavior is much easier to reason about. In many cases you may not even need set_working_directory, as long as the container truly starts in /app.
Still, explicitly setting it in pull can be safer if you want to avoid ambiguity.
Option 3: Install your code as a Python package and use module imports
If your flow code is packaged and importable, you can avoid some file path fragility by making sure the module is installed in the runtime image.
This helps especially when filesystem layout differs between build/deploy and execution environments.
What I would not rely on
- directory: "."
- assuming the deploy-time working directory matches runtime
- absolute file entrypoints like /app/foo.py:flow as a general strategy
In practice, Prefect expects file entrypoints to be resolved from the runtime path context, so the stable pattern is:
- choose a known runtime directory
- set_working_directory to that directory
- keep entrypoint relative to it
Recommended pattern for your case
Since you're using an ECS-style image, I’d recommend:
1. Ensure your image copies the repo into a known location, e.g. /app
2. Set container WORKDIR /app if possible
3. In prefect.yaml, use:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/app"
4. Keep:
yaml
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
That way, deploy image and execution image can differ, and Prefect still resolves the flow from the runtime location you control.
CLI reference
Verified command:
- Prefect docs
- prefect deploy --help
If you want, send me your Dockerfile (or just WORKDIR + COPY lines), and I can tell you the exact directory value to use.Jeff Rose
05/04/2026, 4:54 PMMarvin
05/04/2026, 4:54 PMMarvin
05/04/2026, 4:57 PMentrypoint is not the one you expected
- so rn/...py:vacuum_cleaner ends up being resolved from a nested path
So yes: Prefect can effectively introduce an extra parent/subdirectory layer during execution, even if prefect deploy succeeds.
Important detail
The deployment succeeding only proves Prefect could register the deployment definition.
It does not prove the worker runtime can resolve:
yaml
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
from the final execution cwd.
What to do next
The fastest way to work around this is to inspect the real runtime cwd and filesystem inside the execution container.
If possible, add this at the top of your flow file temporarily:
python
import os
from pathlib import Path
print("CWD:", os.getcwd())
print("FILES:", os.listdir("."))
print("SRC EXISTS:", Path("/src").exists())
if Path("/src").exists():
print("/src:", os.listdir("/src"))
If the flow file never imports, then the issue is happening before your flow code loads, which is even more evidence that the entrypoint path is being resolved from the wrong directory.
In that case, I’d recommend one of these more robust fixes:
Option 1: Avoid file-path entrypoints by packaging your code
If your code is installed into the image as a Python package, Prefect can load it by importability rather than fragile filesystem-relative lookup.
This is often the most reliable fix when runtime path staging is weird.
Option 2: Make the execution image contain the code at a stable path and remove pull-step path dependence
If the code is already baked into the image, avoid mixing that with pull logic that changes directories unexpectedly.
In other words:
- either bake code into image and run from fixed image path
- or fetch code dynamically with pull steps
- but avoid a hybrid setup unless you know exactly how cwd lands
Option 3: Adjust the entrypoint to match the actual final runtime directory
For example, if Prefect leaves you in something like:
text
/src/my-repo
then your entrypoint may need to be relative to that final directory, not /src.
So if rn is actually under /src/my-repo/rn, then:
- working directory should be /src/my-repo
- entrypoint should remain rn/...py:vacuum_cleaner
But if cwd is /src and repo is cloned into /src/my-repo, then the entrypoint may need to be:
yaml
entrypoint: "my-repo/rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
My strongest suspicion in your case
You may have:
- image code at /src/...
- but execution is ending up in /src/<extra-folder>/
or
- execution starts in /src, then Prefect clones/downloads/stages into a child dir and changes into it
That would explain why deployment works but runs fail.
What I need to pinpoint it
Please send one of these:
1. the exact execution error / traceback
2. your full pull: section
3. your Dockerfile WORKDIR and COPY lines
4. the actual directory layout inside the runtime image, e.g.
text
/src/rn/maintenance/redshift/redshift_vacuum_cleaner.py
With that, I can tell you whether the fix is:
- changing entrypoint
- removing set_working_directory
- changing image layout
- or switching to a package/module-based setup
Also, there weren’t any matching GitHub issues/discussions I could find for this exact path-nesting pattern, so I don’t want to guess beyond this without your runtime error.Jeff Rose
05/04/2026, 5:15 PMWorker 'ProcessWorker 72d06a79-d5ea-455f-8a3b-9b1bd59c9fd4' submitting flow run '069f8d30-d991-78aa-8000-e9bbd30b1c59'
Running on worker id: 99cdcffb-eec0-489d-8671-a62395e20028. See worker logs here: <https://app.prefect.cloud/account/a477ad0f-3014-426b-b1cb-48639ac6ac5b/workspace/0204ca3a-2901-4601-bf6e-c9906cdf2107/work-pools/work-pool/rn-etl-services-work-pool-dev/worker/99cdcffb-eec0-489d-8671-a62395e20028>
Opening process...
Completed submission of flow run '069f8d30-d991-78aa-8000-e9bbd30b1c59'
> Running set_working_directory step...
[Errno 2] No such file or directory: '/src'
Unexpected exception encountered when trying to load flow
Traceback (most recent call last):
File "/usr/local/lib/python3.11/dist-packages/prefect/deployments/steps/core.py", line 166, in run_steps
step_output = await run_step(step, upstream_outputs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/deployments/steps/core.py", line 137, in run_step
result = await from_async.call_soon_in_new_thread(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/_internal/concurrency/calls.py", line 374, in aresult
return await asyncio.wrap_future(self.future)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/_internal/concurrency/calls.py", line 399, in _run_sync
result = self.fn(*self.args, **self.kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/deployments/steps/pull.py", line 35, in set_working_directory
os.chdir(directory)
FileNotFoundError: [Errno 2] No such file or directory: '/src'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/dist-packages/prefect/engine.py", line 112, in <module>
flow: "Flow[..., Any]" = load_flow(flow_run)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/flow_engine.py", line 150, in load_flow
flow = run_coro_as_sync(
^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/utilities/asyncutils.py", line 207, in run_coro_as_sync
return call.result()
^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/_internal/concurrency/calls.py", line 365, in result
return self.future.result(timeout=timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/_internal/concurrency/calls.py", line 192, in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
raise self._exception
File "/usr/local/lib/python3.11/dist-packages/prefect/_internal/concurrency/calls.py", line 441, in _run_async
result = await coro
^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/utilities/asyncutils.py", line 188, in coroutine_wrapper
return await task
^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/client/utilities.py", line 69, in wrapper
return await func(client, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/flows.py", line 2816, in load_flow_from_flow_run
output = await run_steps(
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/prefect/deployments/steps/core.py", line 194, in run_steps
raise StepExecutionError(f"Encountered error while running {fqn}") from exc
prefect.deployments.steps.core.StepExecutionError: Encountered error while running prefect.deployments.steps.set_working_directory
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/src"
definitions:
work_pools:
rn-prefect-worker-work-pool: &rn-prefect-worker-work-pool
name: "rn-etl-services-work-pool-{{$env}}"
work_queue_name: "{{$env}}"
job_variables:
image: "384144335252.dkr.ecr.us-east-2.amazonaws.com/{{$service}}-{{$env}}:latest"
task_role_arn: "arn:aws:iam::384144335252:role/{{$service}}-{{$env}}-task-role"
execution_role_arn: "arn:aws:iam::384144335252:role/{{$service}}-{{$env}}-task-exec-role"
name: "{{$service}}-prefect-task-run-{{$env}}"
deployments:
- name: "vacuum_cleaner_{{$env}}"
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
work_pool:
name: "rn-etl-services-work-pool-{{$env}}"
version:
tags:
- "{{$env}}"
- "{{$service}}"
- "vacuum_cleaner"
- "redshift"
description: "Redshift maintenance task - runs vacuum and analyze statements against dw tables."
schedules:
- cron: "0 0 * * *"
timezone: "America/New_York"Marvin
05/04/2026, 5:16 PMMarvin
05/04/2026, 5:16 PM/src does not exist in the execution environment.
This line is the failure:
text
[Errno 2] No such file or directory: '/src'
So Prefect is not inventing /src here — it’s trying to honor your pull step exactly, and the runtime container/process does not have that directory.
Key takeaway
Your deployment environment may have /src, but your execution environment does not.
That means one of these is true:
- the image used for the actual flow run is not the image you think it is
- the image is correct, but code is copied somewhere other than /src
- the process worker is launching in an environment where your filesystem layout differs from the built image
- your Dockerfile does not actually create /src in the final runtime stage
Why deployment still works
Because prefect deploy only registers metadata. The failing part happens later, at flow-run execution time, when Prefect tries to run:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/src"
and /src is missing.
What this means for your current config
This config is only valid if the execution image definitely contains:
text
/src/rn/maintenance/redshift/redshift_vacuum_cleaner.py
Right now, it doesn’t.
How to fix it
You need to align the pull working directory with the actual location of the code inside the runtime container.
For example, if your code is really at /app, use:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/app"
If your code is at /opt/prefect, use that instead.
Best next step: verify the runtime image layout
Check your Dockerfile for lines like:
dockerfile
WORKDIR /src
COPY . /src
or maybe:
dockerfile
WORKDIR /app
COPY . /app
or multi-stage build logic where the final stage does not copy source into /src.
A very common issue is a multistage Docker build like:
dockerfile
FROM builder as build
WORKDIR /src
COPY . /src
...
FROM python:3.11-slim
COPY --from=build /something /something
In that case, /src existed in the builder stage but not the final runtime image.
What I suspect most
You’re probably using an image where:
- prefect deploy or local development sees /src
- but the ECS task image actually does not have /src in the final stage
So the fix is probably in the Dockerfile, not in Prefect.
How to confirm quickly
Run the exact image locally and inspect it:
bash
docker run --rm -it <http://384144335252.dkr.ecr.us-east-2.amazonaws.com/<service>-<env>:latest|384144335252.dkr.ecr.us-east-2.amazonaws.com/<service>-<env>:latest> sh
pwd
ls /
find / -maxdepth 3 -type f | grep redshift_vacuum_cleaner.py
That will tell you the real path of the flow file in the runtime image.
Then set:
- directory to the parent folder containing rn
- keep entrypoint relative to that directory
So if the file is found at:
text
/app/rn/maintenance/redshift/redshift_vacuum_cleaner.py
then use:
yaml
pull:
- prefect.deployments.steps.set_working_directory:
directory: "/app"
entrypoint: "rn/maintenance/redshift/redshift_vacuum_cleaner.py:vacuum_cleaner"
One more important note
Because your log says:
text
Worker 'ProcessWorker ...' submitting flow run ...
Opening process...
this is a process worker execution path. That means the run is being launched as a process from the worker environment, and the path must exist in that runtime environment. If the worker is not actually running inside the image containing your code, the image job variable may not be taking effect the way you expect.
That’s a very important clue.
A plain process worker runs subprocesses on the worker host. It does not automatically mean "run inside this Docker/ECS image" unless the underlying work pool type actually uses containerized infrastructure.
So I’d verify your work pool type first.
What to check right nowMarvin
05/04/2026, 5:16 PMrn-etl-services-work-pool-{{$env}} an ECS work pool or a process work pool?
2. Does the worker host itself contain /src?
3. Is the image job variable actually used by that pool type?
If this is truly a process pool, then image: is likely irrelevant, and your flow is running on the worker machine filesystem, not in your container image.
That would fully explain everything.
If you want, paste the output of:
- prefect work-pool inspect rn-etl-services-work-pool-dev
- or a screenshot of the work pool type in Cloud
and I can tell you whether the image setting is being ignored.Jeff Rose
05/04/2026, 5:22 PMFROM public.ecr.aws/docker/library/python:3.11-slim-bookworm
RUN python -m pip install --upgrade pip
RUN python -m pip install --upgrade setuptools
WORKDIR /src
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install .
(venv) RELAY-435-MBPr:rn-etl-services jrose$ prefect work-pool inspect rn-etl-services-work-pool-dev
WorkPool(
id='82157a01-d48e-4194-bf3d-f93eee762350',
created=DateTime(2025, 8, 20, 16, 53, 58, 557748, tzinfo=Timezone('UTC')),
updated=DateTime(2026, 2, 21, 21, 3, 47, 519960, tzinfo=Timezone('UTC')),
name='rn-etl-services-work-pool-dev',
type='process',
base_job_template={
'variables': {
'type': 'object',
'properties': {
'env': {
'type': 'object',
'title': 'Environment Variables',
'description': 'Environment variables to set when starting a flow run.',
'additionalProperties': {'anyOf': [{'type': 'string'}, {'type': 'null'}]}
},
'name': {
'anyOf': [{'type': 'string'}, {'type': 'null'}],
'title': 'Name',
'default': None,
'description': 'Name given to infrastructure created by a worker.'
},
'labels': {
'type': 'object',
'title': 'Labels',
'description': 'Labels applied to infrastructure created by a worker.',
'additionalProperties': {'type': 'string'}
},
'command': {
'anyOf': [{'type': 'string'}, {'type': 'null'}],
'title': 'Command',
'default': None,
'description': 'The command to use when starting a flow run. In most cases, this should be left blank and the command will be
automatically generated by the worker.'
},
'working_dir': {
'anyOf': [{'type': 'string', 'format': 'path'}, {'type': 'null'}],
'title': 'Working Directory',
'default': None,
'description': 'If provided, workers will open flow run processes within the specified path as the working directory. Otherwise,
a temporary directory will be created.'
},
'stream_output': {
'type': 'boolean',
'title': 'Stream Output',
'default': True,
'description': 'If enabled, workers will stream output from flow run processes to local standard output.'
}
}
},
'job_configuration': {
'env': '{{ env }}',
'name': '{{ name }}',
'labels': '{{ labels }}',
'command': '{{ command }}',
'working_dir': '{{ working_dir }}',
'stream_output': '{{ stream_output }}'
}
},
status=WorkPoolStatus.READY,
storage_configuration=WorkPoolStorageConfiguration(),
default_queue_id='779426c7-0ec5-49e7-a55b-62cd4c261fcc'
)Marvin
05/04/2026, 5:23 PMMarvin
05/04/2026, 5:23 PMtext
type='process'
So the root issue is:
your deployment is running on a process work pool, not an ECS work pool.
That means:
- the flow run is executed as a local subprocess on the worker machine
- the image job variable is ignored
- your Dockerfile and container filesystem are not what the run is using
- /src must exist on the worker host, not in your Docker image
So when you said “it’s ECS”, the actual inspected pool says otherwise. Prefect is trusting the pool type, and the pool type is definitely process.
That also explains this earlier log line:
text
Worker 'ProcessWorker ...'
Opening process...
That was the clue.
Why your image setting isn’t helping
In a process work pool, valid job variables are things like:
- env
- name
- labels
- command
- working_dir
- stream_output
There is no container image execution here.
So this section in your deployment:
yaml
job_variables:
image: "<http://384144335252.dkr.ecr.us-east-2.amazonaws.com/{{$service}}-{{$env}}:latest|384144335252.dkr.ecr.us-east-2.amazonaws.com/{{$service}}-{{$env}}:latest>"
task_role_arn: ...
execution_role_arn: ...
does not apply to a process pool.
Those values are basically not meaningful for this pool type.
What you should do
You have two paths:
Option 1: Use a real ECS work pool
If you want Prefect to run flow runs in your Docker image on ECS, you need an ECS work pool, not a process work pool.
Then your image, task_role_arn, and execution_role_arn fields will make sense.
Right now they do not, because the pool type is wrong for that configuration.
Option 2: Stay with a process pool
Then stop thinking in terms of Docker image layout. Your code must exist on the worker host, and paths must be host paths.
In that case, /src would need to exist on the host where the process worker is running.
But from your goal, it sounds like this is not what you want.
My recommendation
You should switch to an ECS work pool.
Because your deployment config is clearly written for ECS-style execution:
- image
- task_role_arn
- execution_role_arn
That config belongs on an ECS work pool, not a process pool.
Why the current behavior feels inconsistent
Deployment registration works because Prefect accepts the deployment metadata.
Execution fails or behaves strangely because the actual worker type is not the infrastructure type you intended.
So the problem is not really set_working_directory.
The bigger problem is:
the deployment is targeting the wrong work pool type.
What to do next
1. Create or use an ECS work pool
2. Point this deployment at that ECS work pool
3. Then your image filesystem matters, and /src should be valid if the image is built as shown
4. Remove any assumptions based on the current process pool
Useful self-serve paths
- Work pools docs: https://docs.prefect.io/
- Cloud UI: Prefect Cloud
If you want, I can help you rewrite this deployment block for a proper ECS work pool setup.