AGENTS.md · saleor/saleor
AGENTS.md from saleor/saleor: 664 lines covering Deployment model (read this first), Multi-tenancy: never rely on settings.SITEID, Graphql, Running in a git…
Template by saleor
Chalk Labs
Need help deploying AGENTS.md · saleor/saleor?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code. Read the repository before you give any tool access to your data.
curl -fsSL https://raw.githubusercontent.com/saleor/saleor/main/AGENTS.md -o AGENTS.md
Read it and adapt it to your project before use.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: backend-development, code-generation
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
What's in it
An AGENTS.md file: instructions coding agents (Codex, Cursor, Copilot, Gemini CLI and others) read before working in a repo.
- For
- Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code
- Length
- 664 lines
- File
AGENTS.md
Sections
- Deployment model (read this first)
- Multi-tenancy: never rely on settings.SITEID
- Graphql
- Running in a git worktree
- Running tests
- Writing tests
- GraphQL Authorization Tests
- Dispatching webhook events
- Atomic Increment Pattern
- Avoid Check-Then-Act Race Conditions
- Row-Level Locking with selectforupdate
- Database Transactions
- Schedule background work after commit
- Never block a request/worker thread
- Other concurrency rules
- Summary of Patterns Used in Saleor
- Marking search indexes as dirty
- Adding indexes / unique constraints concurrently
- Correctness (Django): use pk instead of id
- Prefer docstrings over comments
First lines (shown under the repo's BSD-3-Clause license)
# Saleor
## Deployment model (read this first)
Saleor is deployed for **horizontal scale-out: many identical Kubernetes pods** (web + Celery
workers) run concurrently behind a load balancer, against a **shared Postgres (with read replicas)
and shared Redis/broker**. Any given request or task may land on any pod, and the same code runs in
many processes at once. Write code with that in mind:
- **No in-process/local state as source of truth.** Never rely on module globals, in-memory caches,
or a value set by a previous request being present on the next one — the next call hits a different
pod. Shared state lives in Postgres and Valkey or Redis.
- **Assume every operation runs concurrently with itself.** Use atomic DB operations
(`F()`, `update_or_create`, `select_for_update`) instead of check-then-act — see *Concurrency and
Thread Safety* below.
- **Never block a request/worker thread.** No `time.sleep()` and no unbounded `while True` retry loops
in request-handling code — a blocked thread wastes a finite pod worker slot. Use bounded retries and
background tasks.
- **Schedule background work after the transaction commits.** Use `on_commit` / `call_event` (not a
bare `.delay()` inside an open transaction) so a task can't run on another pod before the row it
needs is committed.
- **Protect the shared database.** It is the scaling bottleneck: avoid N+1 queries and per-row writes
in loops (bulk instead), keep `select_for_update` transactions short, fetch only the columns you
need, and don't add per-request queries to hot paths — reuse dataloaders.
- **Make tasks idempotent and safe to retry** — the broker may deliver a task more than once, and pods
can be killed/rescheduled mid-run.
## Multi-tenancy: never rely on `settings.SITE_ID`
A deployment may serve **several sites from one process**, and `SITE_ID` may be unset or differ from
what a dev environment has. Never read it directly to identify the current site, and never assume
there is only one.
- **Resolve the site with `Site.objects.get_current()`** (or the `get_site_promise` dataloader in the
GraphQL layer). Both pick the site by `SITE_ID` when it is configured and fall back to the request
host when it is not — `settings.SITE_ID` alone silently collapses every tenant into one.
- **Key anything per-site by the resolved site**, not by a constant. A cache key, module-level dict or
query filter that bakes in a single site lets one tenant read another's data.
- **Don't assume a single row.** `Site.objects.get()` breaks as soon as a second site exists; filter by
the resolved site instead.
`Site.objects.get_current()` reads the patched process-global `THREADED_SITE_CACHE`
(`saleor/site/patch_sites.py`), which is never invalidated by `Site`/`SiteSettings` saves. So a value
read through it can be stale after a write in the same process — see *Writing tests* for what that
means for assertions.
## Graphql
### API versioning
- Check last git tag to find what version we are branched from (e.g. 3.22 tag). Based on that, add
description to new fields with ADDED_IN_{VERSION} clause. Use the version the change actually ships
in — for a backport, that is the backport's version, not the one on `main`.
- Import the constant from `saleor/graphql/core/descriptions.py` and concatenate it onto the
description. Add a new constant there if the target version has none yet.
- **For a new type, annotate only the type-level `Meta.description`.** Its fields are covered by the
type's own annotation, so leave them unstamped:
```python
class TransactionEventFilterInput(BaseInputObjectType):
created_at = DateTimeRangeInput(
description="Filter transaction events by created at date.",
)
class Meta:
doc_category = DOC_CATEGORY_PAYMENTS
description = "Filter input for transaction events data." + ADDED_IN_323
```
- Annotate an individual field only when it is added to a type that already exists. That is the case
where the field's version genuinely differs from its type's.
- Do not stamp a pre-existing field just because you changed its behavior. ADDED_IN marks new schema
surface, not modifications.
- Regenerate `saleor/graphql/schema.graphql` afterwards and commit it — the annotation is part of the
public schema's description text, and a stale schema file fails CI.
### GraphQL permissions
- Use PermissionsField to describe field restrictions
# Architecture
- Do not use Django signals (e.g. `post_save`, `pre_delete`, `@receiver`). Call the relevant logic explicitly from the code that triggers it instead of wiring it through signal handlers. (The one exception is the `post_migrate` signal used to trigger data-migration tasks — see Data Migrations below.)
# Code style
- Use global import statements. Place all imports at the top of the file rather than inside functions, methods, or other local scopes.
# Testing
## Running in a git worktree
When Saleor is checked out in a git worktree, prefer running operations (tests, management
commands, etc.) inside that worktree's containers rather than on the host machine. The host's
services may belong to a different worktree, so running directly on the host can hit the wrong
database/cache or collide with another worktree's stack.
- Use the `.worktree-container/compose.sh` wrapper, which targets the current worktree's
isolated stack. Run commands inside the `saleor` service, e.g.:
```sh
.worktree-container/compose.sh up -d # start this worktree's stack
.worktree-container/compose.sh exec saleor pytest --reuse-db saleor/path/to/test_file.py
.worktree-container/compose.sh exec saleor python manage.py migrate
```
- Do not run these operations directly on the host machine when working in a worktree.
## Running tests
- Run tests using `pytest`
- Attach `--reuse-db` argument to speed up tests by reusing the test database
- Select tests to run by passing test file path as an argument
- Inside the dev container, dependencies are installed system-wide, so run `pytest` directly — no virtual environment to activate
- On the host (outside the container), enter the virtual environment before executing tests
## Writing tests
- Use given/when/then structure for clarity
- Use `pytest` fixtures for setup and teardown
- Declare test suites flat in file. Do not wrapp in classes
- Prefer using fixtures over mocking. Fixtures are usually within directory "tests/fixtures" and are functions decorated with`@pytest.fixture`
- When you create an object for testing and fixture for it doesn't exist, create new one. You can use factory to pass arguments to the fixture.
- When writing assertions, prefer assertion on actual returned value instead of checking if it's not none. For example: `assert email is "a@b.com` instead if `assert email is not None`
- When asserting GraphQL errors, assert error message too
- When asserting to Enum, import enum in a test file and use `assert error.code == MyEnum.SOME_ERROR.name` instead plain string comparison
- Avoid assertion to plain values, if you already have references to existing value. For example, when you create entity in database and assert if entity is returned in response, compare response fields with entity fields, instead plain values.
- When building GraphQL query variables, use enum values instead of plain strings. For example: `{"field": TransactionSortField.CREATED_AT.name}` instead of `{"field": "CREATED_AT"}`
- Do not write unnecessary prefix to test names. If file name is `test_my_mutation.py`, do not write `test_my_mutation_with_x_y` but `test_with_x_y`
- When testing graphQL cases with expected errors ALWAYS assert expected length of errors list
- When setting up test data, extract values into variables and reuse them in assertions. Do not repeat literal values between setup and assertion — use the variable instead.
- When comparing JSON payloads in tests, use `json.loads()` to compare dicts instead of comparing serialized strings with `json.dumps()`. String comparison breaks when key order changes.
- When a test expects exactly one row, use `Model.objects.get()` instead of `.first()` followed by an `is not None` assertion. `get()` both fetches the object and asserts that exactly one exists, so drop the separate not-None check.
- For presence/absence checks prefer `qs.exists()` over `qs.count() == 0` / `!= 0`. `exists()` is cheaper than a `COUNT`. Example: `assert checkout.lines.exists() is False`.
- Use `@pytest.mark.parametrize` for variations of the same scenario instead of copy/pasting near-identical test bodies. Label each case with a leading `_case` string parameter rather than the `ids=` argument — it keeps the description next to the data and is easier to read and edit:
```python
@pytest.mark.parametrize(
("_case", "value"),
[
("omitted", {}),
("empty_list", {"lines": []}),
],
)
def test_something(_case, value): ...
```
- When a mutation input is optional/nullable, test the explicit `null` value in addition to omitting the field — they are distinct inputs and can be handled differently.
- **Never assume a cache is cold — or warm.** Caches are process-global and survive across tests, so
what warmed them depends on test ordering. To assert a write landed, read the row back
(`obj.refresh_from_db(fields=(...))`) rather than through a cached accessor like
`Site.objects.get_current()`; to count queries, warm the cache first so the count is the steady
state; otherwise bust it explicitly (`Site.objects.clear_cache()`). A test that passes only because
a cache happened to be empty will fail as soon as an unrelated caller warms it.
### Assert precisely (the #1 source of review comments)
- **Assert exact values and counts, never bounds or mere absence.** No `len(x) > 0`, `>= 1`,
`assert not errors`, or `call_count >= 1` — assert the exact count, enum, or object so unexpected
extra results are caught. When one error is expected, assert *exactly one*, its full message, and
its `path`/field.
- **On negative / permission-denied tests, also assert the side effect did NOT happen** — balance
unchanged, `SomeEvent.objects.exists() is False`, the object left unmutated. An error assertion
alone doesn't prove the action was blocked.Signals
- Stars
- 23,473
- Forks
- 6,159
- Open issues
- 201
- Last commit (this folder)
- Oct 16, 2026 (14 days ago)
- Last release
- Oct 20, 2026 (10 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- BSD-3-Clause
- Repository
- github.com/saleor/saleor
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get AGENTS.md · saleor/saleor running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
AGENTS.md · saleor/saleor. Agent Mart. https://agentmart.dev/templates/saleor-saleor-agents-md (last updated 2026-10-24)