Flask Apps skill for Codex
From one app.py to a testable Flask factory. Codex turns Flask code into an app factory with config from prefixed environment variables, feature blueprints, and extensions bound through init_app so tests and CLI commands get fresh instances. It explains and fixes application and request context errors, scopes queries by owner, returns JSON errors for APIs, reviews Alembic migrations, and prepares gunicorn and ProxyFix settings for production.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- flask-apps Installs to ~/.agents/skills/flask-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install flask-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project flask-apps Installs to .agents/skills/flask-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/flask-apps.zipunzip -o flask-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) flask-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $flask-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Structures Flask 3 applications with the app factory pattern - create_app with config objects and prefixed environment variables, blueprints per feature, extensions initialised with init_app, Flask-SQLAlchemy 3 and Flask-Migrate, application and request context rules, JSON error handlers, CLI commands, and pytest fixtures with test_client. Use when starting a Flask project, refactoring a single-file app.py into blueprints, fixing "working outside of application context" errors, adding extensions, or preparing Flask for gunicorn in production.
Use it when
- Starting a Flask project or splitting a large app.py
- Adding extensions, blueprints, CLI commands or JSON error handling
- Context errors appear in scripts, threads or tests
- Preparing a Flask app for gunicorn behind a proxy
Use something else when
- You are starting a new async-first JSON API: use fastapi-services
- The question is SQLAlchemy query design rather than Flask wiring: use sqlalchemy-2
How to check the result
pytest passes using create_app with test config, flask routes lists every blueprint under its prefix, and an unknown URL returns the JSON 404.
The mistake it prevents
Importing a module-level app object across files, which causes circular imports and makes tests share one configured instance.
Done well means: the app is built by create_app() so tests and CLI get fresh,
configurable instances; features live in blueprints; extensions are created once at
module level and bound with init_app; no code relies on a global app import; and
configuration comes from the environment with safe defaults only for development.
Inspect first
- Read
AGENTS.md, thenpyproject.toml/requirements.txtfor Flask (3.x assumed), extensions (Flask-SQLAlchemy, Flask-Migrate, Flask-Login, Flask-WTF) and the test runner. - Find how the app is created and run:
rg -n "Flask\(__name__|create_app|FLASK_APP|--app" .. - Find imports of a global app:
rg -n "from .* import app\b" .; these are the circular-import and testing problems the factory removes.
Layout
src/myapp/
__init__.py # create_app()
config.py # Config, TestConfig
extensions.py # db = SQLAlchemy(...), migrate = Migrate(), login = LoginManager()
models.py # or per-blueprint models
errors.py # register_error_handlers(app)
orders/
__init__.py # bp = Blueprint("orders", __name__)
routes.py
service.py
tests/
conftest.py
migrations/ # Flask-Migrate (Alembic)
The factory
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
migrate = Migrate()
# __init__.py
from flask import Flask
def create_app(config: dict | None = None) -> Flask:
app = Flask(__name__)
app.config.from_object("myapp.config.Config")
app.config.from_prefixed_env() # FLASK_SECRET_KEY -> SECRET_KEY, values parsed as JSON when possible
if config:
app.config.update(config) # tests override here
if not app.config.get("SECRET_KEY"):
raise RuntimeError("SECRET_KEY is not set")
from .extensions import db, migrate
db.init_app(app)
migrate.init_app(app, db)
from .orders import bp as orders_bp
app.register_blueprint(orders_bp, url_prefix="/orders")
from .errors import register_error_handlers
register_error_handlers(app)
from . import cli
cli.register(app)
return app
# config.py
class Config:
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db" # overridden by FLASK_SQLALCHEMY_DATABASE_URI
SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True}
MAX_CONTENT_LENGTH = 10 * 1024 * 1024
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
Import blueprints inside the factory to avoid circular imports between blueprints and extensions.
Blueprints and routes
# orders/__init__.py
from flask import Blueprint
bp = Blueprint("orders", __name__)
from . import routes # noqa: E402,F401 (registers routes on bp)
# orders/routes.py
from flask import abort, jsonify, request
from flask_login import current_user, login_required
from ..extensions import db
from ..models import Order
from . import bp
@bp.post("")
@login_required
def create_order():
data = request.get_json(silent=True) or {}
qty = data.get("qty")
if not isinstance(qty, int) or qty < 1:
abort(422, description="qty must be a positive integer")
order = Order(customer_id=current_user.id, sku=str(data.get("sku", "")), qty=qty)
db.session.add(order)
db.session.commit()
return jsonify(id=order.id, sku=order.sku, qty=order.qty), 201
@bp.get("/<int:order_id>")
@login_required
def get_order(order_id: int):
order = db.first_or_404(db.select(Order).filter_by(id=order_id, customer_id=current_user.id))
return jsonify(id=order.id, sku=order.sku, qty=order.qty)
- Use the
<int:...>converters so bad ids 404 before reaching code. - For anything more than a couple of fields, validate with a schema library already in the project (Pydantic, marshmallow) rather than hand checks.
- Scope lookups by owner, as above;
db.get_or_404(Order, id)alone is an IDOR on user-owned data. - Flask-SQLAlchemy 3 prefers
db.session.execute(db.select(...))anddb.get_or_404; the legacyModel.queryinterface still works but do not add new uses.
Contexts
| Error | Cause | Fix |
|---|---|---|
| “Working outside of application context” | Using current_app, db.session or url_for in a script, thread or import-time code | with app.app_context(): or move the code into a request/CLI command |
| “Working outside of request context” | Using request, session outside a request | Pass values in as arguments; in tests use app.test_request_context() |
| Data from one request seen in another | Storing per-request state in module globals | Use g (per app context) or pass explicitly |
Background threads do not inherit contexts. Capture the values you need, or push
app.app_context() inside the thread with the app object passed in. Prefer a real job
queue: see $background-jobs.
Errors
from flask import jsonify
from werkzeug.exceptions import HTTPException
def register_error_handlers(app):
@app.errorhandler(HTTPException)
def http_error(e: HTTPException):
return jsonify(error={"code": e.code, "name": e.name, "message": e.description}), e.code
@app.errorhandler(Exception)
def unhandled(e: Exception):
app.logger.exception("unhandled error")
return jsonify(error={"code": 500, "message": "Internal server error"}), 500
Blueprint-level @bp.errorhandler only handles errors raised in that blueprint’s views,
and does not handle 404s for unmatched URLs (those never reach a blueprint).
CLI commands
# cli.py
import click
def register(app):
@app.cli.command("seed")
@click.option("--count", default=10)
def seed(count: int):
"""Insert demo orders (development only)."""
...
Commands registered on app.cli run inside an app context automatically. Run with
flask --app myapp seed --count 5.
Migrations
flask --app myapp db migrate -m "add orders"
# review migrations/versions/<rev>_add_orders.py before applying
flask --app myapp db upgrade
Alembic autogenerate misses some changes (renames, server defaults, some constraint types); read every script.
Testing
# tests/conftest.py
import pytest
from myapp import create_app
from myapp.extensions import db
@pytest.fixture
def app():
app = create_app({"TESTING": True, "SECRET_KEY": "test",
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", "WTF_CSRF_ENABLED": False})
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
Test with the production database engine in CI if SQL differs (JSON columns, constraints);
SQLite in-memory is fine for logic-only tests. app.test_cli_runner().invoke(args=[...])
tests CLI commands.
Production
gunicorn "myapp:create_app()" --workers 3 --bind 0.0.0.0:8000. Neverflask runordebug=Truein production; the debugger allows code execution.- Behind a proxy, wrap with
werkzeug.middleware.proxy_fix.ProxyFix(app.wsgi_app, x_for=1, x_proto=1)using the real hop count. - Flask 3.1 adds
SECRET_KEY_FALLBACKSfor key rotation andMAX_FORM_MEMORY_SIZE/MAX_FORM_PARTSlimits; setMAX_CONTENT_LENGTHregardless. - Async views work but each request still occupies a WSGI worker; for async-first
services consider Quart or FastAPI (
$fastapi-services).
Verification
pytest -q
flask --app myapp routes # every blueprint route registered with the intended prefix
flask --app myapp db check # Flask-Migrate: fails if models and migrations differ (needs a DB)
Then run the app locally and request one route per blueprint plus an unknown URL (expect the JSON 404, not an HTML page).
Gotchas
app.config.from_prefixed_env()must run afterfrom_objector the defaults overwrite environment values.request.get_json()raises 415 when the Content-Type is not JSON; usesilent=Trueand validate.jsonifyof SQLAlchemy objects fails; serialize explicitly or use a schema.- Flask-Login’s
login_requiredredirects API clients to the HTML login page wheneverlogin_manager.login_viewis set; register anunauthorized_handlerthat returns 401 JSON for API blueprints.
Output template
## Flask change: <summary>
- Factory/config: <changes, new env vars>
- Blueprints: <name, prefix, routes>
- Extensions: <added, init_app placement>
- Migrations: <revision, reviewed>
- Tests: <fixtures, cases> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Flask Apps"
short_description: "From one app.py to a testable Flask factory"
default_prompt: "$flask-apps Refactor our single-file Flask app into an app factory with blueprints" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.