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
- Defining DocTypes - Advanced patterns
- Architecture - System overview