Actions
Reader functions are all about selecting and transforming data coming from the database. They're deliberately tightly bound to the ORM, and in fact they reformulate its concepts and components rather than obscuring them.
Action functions, on the other hand, are the place to put anything that doesn't live in a reader. A straightforward CRUD-ish Django application will use action functions to encapsulate state changes: creating records, updating records, deleting records via calls to the ORM. In a more complex project, actions would wrap any effectful event in the system: enqueuing background jobs, sending emails, reaching out to third-party APIs.
Unlike readers, actions are always explicitly called by code you write, rather than called on your behalf by a framework. For this reason, it's less important (and often impossible) to define a strict framework protocol. The arguments and return types of the action functions in your application will be dictated by the kinds of things your application does.
Most commonly, actions are called from a view as part of an HTTP request/response cycle. This will make more sense when we get to the section on Interfaces, which will include code examples to demonstrate. But actions are also called from other places: management commands, background jobs, and from other actions.
In many ways, actions are closely inspired by the concept of "services" in the HackSoft Django styleguide. HackSoft's guide suggests several "forms" of service (functions, classes, modules), whereas I'd strongly lean towards using functions in all cases. It is also quite prescriptive about how the functions are defined: use keyword-only arguments, and strictly type-annotate the argument and return types. I'd rather leave those decisions up to the team. In my experience, it's unusual for the argument types to be ambiguous if their names are well chosen.
I have also deliberately and explicitly rejected the term "services" because I feel it's too loaded to be useful. Different languages and frameworks already have their own meanings for "services", "service layers" and "service-oriented architectures", and so there's a big risk of confusion. Actions is a clearer term: action functions encapsulate, literally, the actions your system can take in the world.
There's also a more subtle suggestion to make about exceptions and data validation. This will become clearer when we get to the section on Interfaces, but it's worth outlining here. Once data gets inside an action function, it should already have been validated with respect to the definition of "correctness" defined by the application. In other words, if the action function is expecting an argument name to be a string, and that string comes from outside the boundary of the application (in other words, if it's submitted as part of an HTTP request), it must be validated as being a string (and a string of the correct length, and so on) before the action is called.
This approach allows us to draw a boundary between input-level correctness (checked pre-action) and system-level correctness (handled inside the action). This also maps neatly onto HTTP response codes: a validation error produced outside the action usually results in a 4xx response to the client, and any exception raised inside the action usually results in a 5xx response to the client.
This error handling strategy is not perfect: there will be cases where the abstraction "leaks" and the calling code ends up having to catch errors from inside the action and propagate them to the client in a more useful way. But as a rule of thumb, starting with this approach should cover, say, 99% of examples quite happily.
Summary
Action functions encapsulate the business logic that isn't covered by readers. Their most common use is to enact state changes in the database, but they may also wrap up other side-effects such as interacting with external systems.