Skip to main content

Creating Apps

A guide to building Framework M applications.

Prerequisites

  • Python 3.12+
  • uv package manager
  • Framework M installed

Create a New App

# From your project root
uv run m new:app my_crm

This creates:

my_crm/
├── src/
│ └── doctypes/
│ └── __init__.py
├── pyproject.toml
└── README.md

Add a DocType

cd my_crm
uv run m new:doctype Contact

This creates:

src/doctypes/contact/
├── __init__.py
├── doctype.py # Schema definition
├── controller.py # Business logic
└── test_contact.py # Tests

Define Fields

Edit src/doctypes/contact/doctype.py:

from __future__ import annotations

from typing import ClassVar
from pydantic import Field
from framework_m.core.domain.base_doctype import BaseDocType


class Contact(BaseDocType):
"""Customer contact information."""

__doctype_name__: ClassVar[str] = "Contact"

# Required fields
first_name: str = Field(description="First name")
last_name: str = Field(description="Last name")

# Optional fields
email: str = Field(default="", description="Email")
phone: str = Field(default="", description="Phone")
company: str = Field(default="", description="Company name")

class Meta:
naming_rule: ClassVar[str] = "autoincrement"

Add Business Logic

Edit src/doctypes/contact/controller.py:

from __future__ import annotations

from framework_m.core.domain.base_controller import BaseController
from .doctype import Contact


class ContactController(BaseController[Contact]):
"""Contact business logic."""

async def validate(self, context=None):
"""Validate before saving."""
if not self.doc.first_name.strip():
raise ValueError("First name is required")

# Normalize email
if self.doc.email:
self.doc.email = self.doc.email.lower().strip()

async def after_save(self, context=None):
"""Called after save."""
# Example: Log or send notification
pass

Run Migrations

# Initialize Alembic (first time)
uv run m migrate init

# Create migration for your DocType
uv run m migrate create "Add Contact doctype" --autogenerate

# Apply migration
uv run m migrate

Start Studio

uv run m studio

Open http://localhost:9000 to manage your data.

Test Your DocType

Edit src/doctypes/contact/test_contact.py:

import pytest
from .doctype import Contact
from .controller import ContactController


def test_contact_creation():
contact = Contact(
first_name="John",
last_name="Doe",
email="John@Example.com",
)
assert contact.first_name == "John"


@pytest.mark.asyncio
async def test_validate_normalizes_email():
contact = Contact(
first_name="John",
last_name="Doe",
email="John@Example.com",
)
controller = ContactController(contact)
await controller.validate()

assert contact.email == "john@example.com"


@pytest.mark.asyncio
async def test_validate_rejects_empty_name():
contact = Contact(
first_name=" ",
last_name="Doe",
)
controller = ContactController(contact)

with pytest.raises(ValueError, match="First name is required"):
await controller.validate()

Run tests:

uv run pytest src/doctypes/contact/

Multiple DocTypes

For related DocTypes, create a directory structure:

src/doctypes/
├── __init__.py
├── contact/
│ ├── doctype.py
│ └── controller.py
├── lead/
│ ├── doctype.py
│ └── controller.py
└── opportunity/
├── doctype.py
└── controller.py

Next Steps