Skip to main content

First-class Django integration for Palm Engine — ORM storage, auto-discovery, and management commands

Project description

palm-django

PyPI version Python License: MIT

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 bootstrapApplicationHost starts on AppConfig.ready() with Django-tuned defaults
  • ORM storage — Palm definitions, instances, and KV entries persist in Django models
  • Auto-discoverypalm_definitions / palm hooks in each installed app
  • Model resources@as_palm_resource exposes CRUD as {app}.{model}.{action}
  • Auto schemas — optional schema=True generates Palm DictStateSchema from Django fields
  • Transactional bridgingpalm_atomic() rolls back ORM + Palm writes together
  • Operator CLIpython manage.py palm doctor|server|run|flow|instance|resource|quickstart
  • Palm Explorerpalm server serves the SSR hub at /explorer
  • Django Admin — inspect definitions, instances, and storage; start flows from admin

Requirements

Installation

pip install palm-django

This installs palmengine (0.12.9+) 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_schema to each ResourceDefinition
  • Registers reusable StateSchemaDefinition entries (myapp.order.data, myapp.order.instance)
  • Validates create/update payloads in DjangoModelProvider (disable with schema={"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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

palm_django-0.8.2.tar.gz (42.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

palm_django-0.8.2-py3-none-any.whl (45.8 kB view details)

Uploaded Python 3

File details

Details for the file palm_django-0.8.2.tar.gz.

File metadata

  • Download URL: palm_django-0.8.2.tar.gz
  • Upload date:
  • Size: 42.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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}

File hashes

Hashes for palm_django-0.8.2.tar.gz
Algorithm Hash digest
SHA256 1b4a6344934324ddb94d6c94bb217519ed440389edc3938f66f704c02e57f62d
MD5 24eb6a80dca4b3cea7f8d52077579bd5
BLAKE2b-256 4c26ecc1e45e19cfb0d5ab00e562a66a1922a7721405855c560ac75452c99573

See more details on using hashes here.

File details

Details for the file palm_django-0.8.2-py3-none-any.whl.

File metadata

  • Download URL: palm_django-0.8.2-py3-none-any.whl
  • Upload date:
  • Size: 45.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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}

File hashes

Hashes for palm_django-0.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3f13db2dc76c9daa39eb5249ec777908bab9d892ff7913e58525a7889d307b92
MD5 faa8aa7d8542e7cebb8a33405a923a65
BLAKE2b-256 57c7ccb3d7db2488d3c01869511dd515e04500b2a4135f9d4f7e6eea5dc5857a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page