Prefect is the worlds best orchestrator. Modal is the worlds best serverless compute framework. It only makes sense that they would fit together perfectly.

In this short guide, we’ll walk you through building your first prefect deployment that runs on modal every hour, and build out a scalable framework to grow your project.

You can see all the functioning code here: https://github.com/Ben-Epstein/prefect-modal

We’ll walk step-by-step through the following:

  1. Create a prefect account
  2. Create a modal account
  3. Create your github repo
  4. Give prefect access to your github repo
  5. Connect prefect and modal
  6. Configure CI/CD

Let’s quickly look at an architecture diagram of what we’re building.

We’ll be building a full serverless infrastructure framework for scheduling and provisioning workflows. GitHub will act as the source of truth, Prefect will be our orchestrator, and Modal will be our serverless infrastructure, paying for only the CPU time we use.

Let’s quickly recap on some important terms:

  • Flow: Some python function in a file, decorated with @flow, that is an entrypoint to do some task in prefect
  • Run: A instance of a flow, executed at some time with some payload.
  • Deployment: A manifest around a flow, including where it should run, when it should run, and any variables it needs to be able to run. This can “schedule” your flows, creating runs when needed.
  • Work pool: A collection of hardware that Prefect can talk to in order to schedule a given flow-run.

1. Create Prefect Account

Simply go to the prefect sign-in page. We suggest using GitHub to manage your account, but any will do!

2. Create Modal Account

Simply go to the modal sign-up page. Easiest to use the same account, but that’s your call!

3. Create your GitHub repo

We’re going to create a simple GitHub repo with our prefect flows. You can follow along here: https://github.com/Ben-Epstein/prefect-modal

This repo will have:

  • Our prefect.yaml deployment config
  • Standard python tooling
  • CI/CD

Let’s start with a Makefile to keep things easy

export PYTHONPATH = .venv

.PHONY: uv
uv:
	pip install --upgrade 'uv>=0.5.6,<0.6'
	uv venv

setup:
	@if [ ! -d ".venv" ] || ! command -v uv > /dev/null; then \
		echo "UV not installed or .venv does not exist, running uv"; \
		make uv; \
	fi
	@if [ ! -f "uv.lock" ]; then \
		echo "Can't find lockfile. Locking"; \
		uv lock; \
	fi
	uv sync --all-extras --all-groups
	uv pip install --no-deps -e .

We’ll configure and manage our environment with uv, which will get configured when you run make setup

Start by running uv init --lib --name prefect_modal --python 3.12 which will create your python project.

If uv creates a python-version that is pinned to a micro (ie 3.12.6) remove the micro, and make it 3.12. This causes issues downstream if not

Next, we’ll add our dependencies by running uv add modal 'prefect[github]'

We need to make one small change to the pyproject.toml created by uv. At the bottom, add the following

[tool.hatch.build.targets.wheel]
packages = ["src/prefect_modal"]

And then we’ll initialize our prefect environment with uv run prefect init

On the first question, select git

Now, because this is meant to be a scalable repo, we’ll create a submodule just for our workflows.

mkdir src/prefect_modal/flows
touch src/prefect_modal/flows/__init__.py

this is where we’ll keep different flows.

Let’s take a look at that prefect.yaml file that was generated.

# Welcome to your prefect.yaml file! You can use this file for storing and managing
# configuration for deploying your flows. We recommend committing this file to source
# control along with your flow code.

# Generic metadata about this project
name: prefect-modal
prefect-version: 3.1.15

# build section allows you to manage and build docker images
build: null

# push section allows you to manage if and how this project is uploaded to remote locations
push: null

# pull section allows you to provide instructions for cloning this project in remote locations
pull:
- prefect.deployments.steps.git_clone:
    repository: https://github.com/Ben-Epstein/prefect-modal
    branch: main
    access_token: null

# the deployments section allows you to provide configuration for deploying flows
deployments:
- name: null
  version: null
  tags: []
  description: null
  schedule: {}
  flow_name: null
  entrypoint: null
  parameters: {}
  work_pool:
    name: null
    work_queue_name: null
    job_variables: {}

The important sections here are pull, which defines the repo and branch to pull from, as well as deployments, which define the flows that will be run at some schedule on a work_pool. The work pool is going to be modal, but we’ll get to that.

Let’s also create our flow now. touch src/prefect_modal/flows/flow1.py

You can do anything in your flow, but we’ll keep it simple

from prefect import flow, task

@task()
def do_print(param: str) -> None:
    print("Doing the task")
    print(param)

@flow(log_prints=True)
def run_my_flow(param: str) -> None:
    print("flow 2")
    do_print(param)

@flow(log_prints=True)
def main(name: str = "world", goodbye: bool = False):
    print(f"Hello {name} from Prefect! 🤗")
    run_my_flow(name)
    if goodbye:
        print(f"Goodbye {name}!")

4. Give prefect access to your github repo

First, let’s auth into prefect: uv run prefect auth login — follow the instructions provided.

We’ll do this via a Personal Access Token (PAT). We will use a classic token here but you can also configure a fine-grained token. Go your your token settings here: https://github.com/settings/tokens and click “Generate a new token (classic)”

Give it a name, an expiration date, and repo access. Then, copy that token.

Now we’ll give prefect that token. First run

uv run prefect block register -m prefect_github

then

uv run prefect block create prefect_github

This command will return a URL. Click that URL to give your block a name and paste in the PAT you got from GitHub. Call the block name prefect-modal to follow along with the repo.

Let’s go back to our prefect.yaml under pull: and make a few changes.

Prefect creates the yaml with the key access_token. Update that key in the yaml to the section to use the key name credentials instead of token

pull:
- prefect.deployments.steps.git_clone:
    id: clone-step
    repository: https://github.com/Ben-Epstein/prefect-modal
    branch: main
    credentials: '{{ prefect.blocks.github-credentials.prefect-modal }}'

Don’t forget to change the repository name to yours!

We’ll add another section now to get ahead of it, which configures how you install your package and what your working directory is

- prefect.deployments.steps.run_shell_script:
    directory: '{{ clone-step.directory }}'
    script: pip install --upgrade 'uv>=0.5.6,<0.6'
- prefect.deployments.steps.run_shell_script:
    directory: '{{ clone-step.directory }}'
    script: uv sync --no-editable --no-dev

Typically uv sync creates a new virtual environment, but we can’t have that for prefect’s sake. It needs to be root python. You’ll see below how to tell uv to use the root python version

What we’ve done here is tell prefect how to download the code from github into the modal pod before running the flow. This is important: we are telling modal to load the code from main. This means that if you push new code to main, the next run of a flow in modal will change to whatever is in main. To keep this stable, you can set that to a release branch, for example, only updating that release branch when you’re stable and ready.

Let’s add a few more things to our deployment in order to get it ready: entrypoint and schedule. We’ll also add a description:

  name: prefect-modal-example
  description: "Our first flow which runs on modal"
  entrypoint: prefect_modal.flows.flow1:main
  schedules:
  - cron: '0 12 * * *'
    timezone: America/New_York
    active: true

We can add more schedules as desired. Simply add another list element. This one runs once a day at 12PM EST.

Because we reference the entrypoint as a module and not a path, it can be called from anywhere (so long as it’s installed). This is much more flexible

5. Connect prefect and modal

What we’re doing here is giving Prefect access to your modal account to provision a serverless instance to run your flows and tasks. Modal will be our work pool in Prefect terms, remaining off when unused, and spinning up the infrastructure we need when a new run of a flow is scheduled. Prefect makes this incredibly easy.

First, authenticate into modal and create a token

uv run modal setup

then

uv run modal token new

then finally

uv run prefect work-pool create --type modal:push --provision-infra pref-modal-pool

That final command, with name pref-modal-pool is custom, you can name it whatever you want. But we’ll take that and update our prefect.yaml with it.

...
work_pool:
    name: "pref-modal-pool"
    work_queue_name: "default"
    job_variables: {
        "env": {"UV_PROJECT_ENVIRONMENT": "/usr/local"}
    }

This UV_PROJECT_ENVIRONMENT is how we tell uv to use the root python. We could pip install without uv but it would be significantly slower (minutes vs seconds) and you wouldn’t get to leverage your lockfile, resulting in unexpected behavior!

We can create multiple work queues across the same pool, for example if we have another job using this pool. This can let you handle concurrency and swim-lane priorities, but we’ll use the default queue name that comes with every new pool.

When we deploy this to prefect, it will now know to schedule all of the flow runs for this particular deployment on our modal work pool!

At this point, you could git add . && git commit -m "prefect modal" && git push to get our code into GitHub, and then uv run prefect deploy --all to get our deployment live, but we want to take the final step and automate Prefect deployments.

6. Configure CI/CD

Our last step. Since our code must exist in GitHub in order for modal to download, install, and run it, we want github to also be our source of truth in terms of telling Prefect what to actually do with our code.

This is simple, with additional details here: https://docs.prefect.io/v3/deploy/infrastructure-concepts/deploy-ci-cd

We first need to add 2 secrets to our GitHub repo: PREFECT_API_KEY and PREFECT_API_URL

For the PREFECT_API_URL, the format is as follows

https://api.prefect.cloud/api/accounts/{account_id}/workspaces/{workspace_id}

Go to your prefect dashboard and from the URL you will find the account it and the workspace id.

Note: the prefect console url uses /account and /workspace but the api uses the plurals (see above).

First, go to your API Keys and generate a new set.

Add this key as a secret in your github repo. Go to your repo, then settingssecrets and variablesactions

Now, we’ll add a simple github workflow. Create a new folder in the root of your github repo: mkdir -p .github/workflows and then create your workflow touch .github/workflows/prefect_cd.yaml and add the following

name: Deploy Prefect flow

on:
  push:
    branches:
      - main

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Prefect Deploy
        env:
          PREFECT_API_KEY: ${{ secrets.PREFECT_API_KEY }}
          PREFECT_API_URL: ${{ secrets.PREFECT_API_URL }}
        run: |
          make prefect-deploy

Now, what’s make prefect-deploy? We’re going to add a new command to our Makefile to keep things very easy

prefect-deploy: setup
    uv run prefect deploy --prefect-file prefect.yaml --all

You can also run that locally at any time to deploy the flows in your deployment. This keeps everything consistent.

Before pushing our code, let’s run make prefect-deploy once to make sure everything works. Prefect will also suggest any changes to our prefect.yaml which we can accept before pushing.

We said yes and let prefect override our deployment spec with a few changes. This will let our CD run without any interruptions or prompts. If we go to our Prefect console, we’ll now see it!

There are no runs yet. We can see a command at the bottom of our terminal from make prefect-deploy to run our deployment-flow, but since our code isn’t in GitHub yet, this will fail. Let’s push up our code, and checkout the action, which should now pass!

And we will see that in the Prefect console as well.

Now, at 12pm the flow will run! But let’s see it run in realtime 😄

First, open up your modal dashboard: https://modal.com/apps/ on your screen next to your prefect dashboard of your flow:

Now, back at your terminal, run uv run prefect deployment run main/prefect-modal-example

You should almost instantly see the run start in your Prefect dashboard. After 30 sec - 1 min, you should see it run in modal! You can click on it to see the logs.

Clicking into the app and then clicking Logs on the left will show everything that’s happening inside the flow run!

You can click on a specific log line to get more details

If you copy the URL in that log, you’ll see the Prefect side

You’re done! 🚀

As you make changes to either the code or deployment file, prefect will redeploy and modal will automatically pick up changes. Schedule away!