palm-django
First-class Django integration for Palm Engine — the Python-first Behavior Tree orchestrator.
Install with one command, add palm_django to INSTALLED_APPS, and run flows, wizards, and resources against your existing Django database.
Features
- Zero-config bootstrap —
ApplicationHoststarts onAppConfig.ready()with Django-tuned defaults - ORM storage — Palm definitions, instances, and KV entries persist in Django models
- Auto-discovery —
palm_definitions/palmhooks in each installed app - Model resources —
@as_palm_resourceexposes CRUD as{app}.{model}.{action} - Auto schemas — optional
schema=Truegenerates PalmDictStateSchemafrom Django fields - Transactional bridging —
palm_atomic()rolls back ORM + Palm writes together - Operator CLI —
python manage.py palm doctor|server|run|flow|instance|resource|quickstart - Palm Explorer —
palm serverserves the SSR hub at/explorer - Django Admin — inspect definitions, instances, and storage; start flows from admin
Requirements
- Python 3.11+
- Django 4.2+
- palmengine 0.13.13+
Installation
pip install palm-django
This installs palmengine (0.13.13+) and Django (4.2+) automatically.
From source (development):
git clone https://github.com/JGabrielGruber/palm-django.git
cd palm-django
pip install -e ".[dev]"
Quick start
1. Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
# ...
"palm_django",
]
On startup, palm_django bootstraps a process-wide ApplicationHost and scans your Django apps for Palm hooks.
2. Run migrations
Django ORM is the default storage backend. Apply palm-django tables before first use:
python manage.py migrate palm_django
3. Configure Palm (optional)
Use a PALM dict and/or individual PALM_* settings. Keys accept either STORAGE_BACKEND (Django style) or storage_backend (Palm style).
# settings.py
PALM = {
# Default is django ORM — override only when needed:
# "STORAGE_BACKEND": "memory",
"LOAD_EXAMPLE_DEFINITIONS": False,
"HOST_PROFILE": "all_in_one",
}
# palm-django integration options (not forwarded to PalmSettings)
PALM_AUTO_START = True
PALM_DISCOVERY_MODULES = ("palm_definitions", "palm")
Defaults are tuned for Django projects (no bundled Palm examples, lightweight startup).
4. Scaffold with quickstart (optional)
python manage.py palm quickstart
python manage.py palm quickstart --app myapp
python manage.py palm quickstart --app myapp --write # writes myapp/palm_definitions.py
5. Register definitions in your apps
Create myapp/palm_definitions.py:
from palm.common.persistence.definition_repository import DefinitionRepository
from palm.definitions.flow import FlowDefinition
def register_definitions(repository: DefinitionRepository) -> None:
repository.register_flow(
FlowDefinition(
id="hello_flow",
name="hello_flow",
pattern="dag",
options={"name": "hello_flow"},
)
)
palm_django imports register_definitions from each installed app automatically.
Supported hooks (all optional, per app):
| Module suffix | Hook |
|---|---|
palm_definitions |
register_definitions() |
palm_definitions |
register_resources() |
palm_definitions |
register_commit_handlers() |
palm |
same hooks (alternate name) |
6. Expose Django models as Palm resources
Decorate any model (or set a class-level palm_resource dict) and palm-django auto-registers CRUD resources at bootstrap:
from django.db import models
from palm_django import as_palm_resource
@as_palm_resource(actions=["get", "create", "update", "delete", "list"])
class Order(models.Model):
customer_id = models.IntegerField()
total = models.DecimalField(max_digits=12, decimal_places=2)
Resource names follow {app_label}.{model_name}.{action} — e.g.
myapp.order.create. Params bind from Palm state via {{ state.pk }},
{{ state.data }}, etc.; results promote to output_key (defaults to the
model name) in wizard/BT leaves.
from palm_django import get_app
app = get_app()
result = app.invoke_resource(
"myapp.order.create",
state={"data": {"customer_id": 1, "total": "49.99"}},
)
7. Wizard + Django model resources
Use Palm 0.12 step_kind: resource with resource_ref pointing at your auto-registered model resources:
FlowDefinition(
id="flow-onboard-order",
name="onboard_order",
pattern="wizard",
options={
"include_summary": True,
"steps": [
{
"slug": "customer_id",
"title": "Customer",
"prompt": "Enter customer id",
"validation": [{"rule": "not_empty"}],
},
{
"slug": "create-order",
"title": "Create order",
"step_kind": "resource",
"resource_ref": "myapp.order.create",
"action": "create",
"params": {
"data": {
"customer_id": "{{ state.customer_id }}",
"total": "{{ state.total }}",
}
},
"output_key": "order",
},
],
},
)
See tests/palm_sample/palm_definitions.py for a full working example (item_wizard).
8. Use Palm in your code
from palm_django import get_host, palm_atomic
def start_onboarding(user_id: str):
return get_host().submit_flow("onboard", metadata={"user_id": user_id})
# Roll back Palm storage + ORM writes together
with palm_atomic():
order = Order.objects.create(customer_id=1, total="10.00")
get_host().submit_flow("fulfill_order", metadata={"order_id": order.pk})
Or access the infrastructure layer directly:
from palm_django import get_app
flows = get_app().list_flows()
9. Operator commands
# Health check (human-readable or JSON)
python manage.py palm doctor
python manage.py palm doctor --json
# Palm Explorer (ServerRuntime + SSR hub) — foreground until Ctrl+C
python manage.py palm server
python manage.py palm host server # alias
python manage.py palm server --port 9000
python manage.py palm server --host 0.0.0.0 --port 8080
# Scaffold snippets
python manage.py palm quickstart --app myapp
# Start a flow or process (auto-detects kind)
python manage.py palm run sample_flow
python manage.py palm flow start onboard --metadata '{"user_id": 42}'
# Inspect catalog
python manage.py palm flow list
python manage.py palm instance list
python manage.py palm instance list --all
python manage.py palm resource list
# Invoke a resource with state binding
python manage.py palm resource invoke myapp.order.create \
--state '{"data": {"customer_id": 1, "total": "10.00"}}'
# Resume a persisted instance
python manage.py palm instance resume <instance_id>
Commands bootstrap Palm automatically and run against the active Django database.
Palm Explorer server
palm server starts a full ServerRuntime with the Palm Explorer SSR surface. It uses your Django database (ORM storage), discovered flows/resources, and PALM_* settings:
python manage.py palm server
# Palm Explorer available at http://127.0.0.1:8080/explorer
Configure bind address via PALM_SERVER_HOST / PALM_SERVER_PORT or CLI --host / --port. Press Ctrl+C for graceful shutdown.
10. Django Admin
Add django.contrib.admin to INSTALLED_APPS to inspect Palm persistence models:
- Palm definitions — browse flows/processes/resources; admin action Start flow
- Palm process instances — browse instances; admin action Resume
- Palm storage entries — raw KV rows (projections, indexes, outbox)
python manage.py palm doctor # confirms admin registration status
Public API
| Symbol | Description |
|---|---|
get_host() |
Process-wide ApplicationHost |
get_app() |
PalmApp infrastructure layer |
get_runtime() |
PalmRuntime wrapper with discovery metadata |
bootstrap_palm() |
Idempotent manual bootstrap |
shutdown_palm() |
Graceful shutdown |
is_palm_started() |
Whether the host is running |
get_palm_settings() |
PalmSettings built from Django settings |
build_palm_settings_dict() |
Raw merged settings dict |
DjangoStorageBackend |
Palm BaseBackend backed by Django ORM |
palm_atomic() |
Context manager for transactional Palm + Django writes |
django_atomic() |
Join outer atomic() without redundant savepoints |
storage_health_report() |
ORM table readiness and row counts |
as_palm_resource |
Decorator to expose a Django model as Palm resources |
DjangoModelProvider |
Palm provider (django_model) backing ORM CRUD |
palm_resource_invoked |
Signal after successful provider invocation |
palm_model_saved |
Signal when a decorated model is saved via ORM |
PalmResourceModel |
Optional base class for class-level palm_resource config |
Django model resources
| Action | Params | State binding examples |
|---|---|---|
get |
model, pk (or custom lookup_field) |
state.pk |
list |
model, filters, order_by, limit |
state.filters |
create |
model, data |
state.data |
update |
model, pk, data |
state.pk, state.data |
delete |
model, pk |
state.pk |
Provider registry key: django_model. Mutating actions join the current Django transaction when one is active.
Auto-generated schemas
Enable Palm-compatible DictStateSchema documents from Django fields with schema=True:
@as_palm_resource(actions=["create", "get"], schema=True)
class Order(models.Model):
customer_id = models.IntegerField()
total = models.DecimalField(max_digits=12, decimal_places=2)
Or via class config:
class LineItem(models.Model):
class PalmResource:
actions = ["create"]
schema = True
When enabled, palm-django:
- Attaches
input_schema/output_schemato eachResourceDefinition - Registers reusable
StateSchemaDefinitionentries (myapp.order.data,myapp.order.instance) - Validates
create/updatepayloads inDjangoModelProvider(disable withschema={"validate": False})
Use schema refs in wizard steps:
{
"slug": "order_data",
"state_schema_ref": "myapp.order.data",
}
Programmatic access:
from palm_django.resources import (
get_palm_resource_config,
model_data_schema_name,
model_to_dict_state_schema,
)
config = get_palm_resource_config(Order)
schema = model_to_dict_state_schema(Order, config, writable_only=True)
ref = model_data_schema_name(Order, config) # myapp.order.data
Signals
Connect to Palm lifecycle events in your Django apps:
from django.dispatch import receiver
from palm_django import palm_model_saved, palm_resource_invoked
@receiver(palm_resource_invoked)
def on_resource_invoked(sender, provider, action, model_label, params, result, **kwargs):
audit_log.info("palm %s %s on %s", provider, action, model_label)
@receiver(palm_model_saved)
def on_model_saved(sender, instance, created, model_label, **kwargs):
if created:
notify_team(instance)
palm_model_saved fires for direct ORM saves on decorated models. Saves performed inside Palm provider mutations emit palm_resource_invoked instead (no duplicate model signal).
Storage
When palm_django is installed, django is the default storage_backend. Palm's key-value contract is preserved:
| Palm key pattern | Django model |
|---|---|
palm:definitions:{kind}:{id} |
PalmDefinition |
palm:instances:{instance_id} |
PalmProcessInstance (snapshots + status history in data) |
| Indexes, projections, outbox, other keys | PalmStorageEntry |
Override with PALM_STORAGE_BACKEND = "memory" or "filesystem" when needed.
Transaction bridging
palm_atomic() and internal django_atomic() join an existing transaction.atomic() block instead of opening redundant savepoints. Palm storage writes and model provider mutations roll back with surrounding Django work:
from django.db import transaction
from palm_django import get_app, palm_atomic
with transaction.atomic():
app.invoke_resource("myapp.order.create", state={...})
# raises → ORM row and Palm KV writes roll back together
Settings reference
Palm settings (PALM dict / PALM_* attrs)
All fields from PalmSettings are supported. Common ones:
| Key | Default (Django) | Notes |
|---|---|---|
storage_backend |
django |
Django ORM via palm_django models |
load_example_definitions |
False |
Avoid Palm demo definitions in production |
host_profile |
all_in_one |
Collapsed embedded runtime |
server_host |
127.0.0.1 |
Bind host for palm server |
server_port |
8080 |
Bind port for palm server |
default_scheduler |
inline |
Synchronous in-process execution |
palm-django integration settings
| Setting | Default | Description |
|---|---|---|
PALM_AUTO_START |
True |
Start ApplicationHost in AppConfig.ready() |
PALM_DISCOVERY_MODULES |
("palm_definitions", "palm") |
Module suffixes to scan per app |
PALM_DISCOVER_DEFINITIONS |
True |
Call register_definitions hooks |
PALM_DISCOVER_RESOURCES |
True |
Call register_resources hooks |
PALM_DISCOVER_COMMIT_HANDLERS |
True |
Call register_commit_handlers hooks |
Common patterns
| Goal | Approach |
|---|---|
| CRUD from flows/wizards | @as_palm_resource + step_kind: resource |
| Custom resource logic | register_resources() with ResourceDefinition |
| Atomic business transaction | with palm_atomic(): around ORM + Palm calls |
| React to Palm writes | @receiver(palm_resource_invoked) |
| React to direct ORM saves | @receiver(palm_model_saved) |
| Ops / debugging | python manage.py palm doctor |
| Browser Explorer | python manage.py palm server |
| New project bootstrap | python manage.py palm quickstart --app myapp |
Troubleshooting
| Symptom | Fix |
|---|---|
storage tables are missing |
python manage.py migrate palm_django |
ApplicationHost is not started |
Run migrations; check PALM_AUTO_START; run palm doctor |
| Resource not found | Use palm resource list; names are {app_label}.{model}.{action} |
Unknown Django model |
Use app_label.ModelName (e.g. myapp.Order), not dotted module path |
| Wizard resource step fails | Use step_kind: resource + resource_ref; set step action (e.g. create) — defaults to fetch if omitted |
| Flow not listed | Add myapp/palm_definitions.py with register_definitions() |
| Admin models missing | Add django.contrib.admin to INSTALLED_APPS |
| DB access during app init warning | Harmless during bootstrap before migrations; disappears after migrate |
python manage.py palm doctor # full report + next-step tips
python manage.py palm doctor --json # machine-readable output
Development
git clone https://github.com/JGabrielGruber/palm-django.git
cd palm-django
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check .
The tests/palm_sample/ app demonstrates discovery, model resources, wizard flows, commands, admin, signals, schemas, and transaction bridging.
Publishing (maintainers)
Same flow as palmengine: tag → GitHub release → CI publishes to PyPI.
Prerequisites: uv, just (optional), and repository secrets PYPI_TOKEN / TEST_PYPI_TOKEN.
# Bump version in pyproject.toml and palm_django/__init__.py; update CHANGELOG.md
just release-prep # ruff + pytest + uv build — see RELEASE-0.8.0.md
git add -A
git commit -m "Release 0.8.0"
git tag -a v0.8.0 -m "palm-django 0.8.0"
git push origin master --tags
On GitHub: Releases → Draft a new release from tag v0.8.0, paste the [0.8.0] section from CHANGELOG.md, and Publish release. The publish workflow uploads to PyPI automatically.
Manual options:
just publish-test # TestPyPI (TEST_PYPI_TOKEN)
just publish # production PyPI (PYPI_TOKEN)
Or trigger Actions → Publish to PyPI → Run workflow (testpypi or pypi) without creating a release.
Links
License
MIT — see LICENSE.
Metadata
Release files for palm-django 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| palm_django-0.9.0.tar.gz | 42.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| palm_django-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 88.6 kB
Release files / palm_django-0.9.0.tar.gz
| Download URL | palm_django-0.9.0.tar.gz |
|---|---|
| Size | 42.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5f91878a5d878fee326e3de560f25721f73138fd3c8327280a1fb75bd9e5b3c8
|
|
BLAKE2b-256 checksum How to use checksums |
ddc1ff7187bc51a01a199ec210546d3b9e1768629221bcb3fe7bba94c18e8334
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / palm_django-0.9.0-py3-none-any.whl
| Download URL | palm_django-0.9.0-py3-none-any.whl |
|---|---|
| Size | 45.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7c156c210d07145f786024cfef414b05c946ec56bb2fce53005750e5773402b6
|
|
BLAKE2b-256 checksum How to use checksums |
7db72fdf99aa6904dffc12faf6a31c75328d65f081fe30ddc4210046544c2ebb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|