diff --git a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst
index 92539670e..f4d2b76c2 100644
--- a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst
+++ b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst
@@ -1477,10 +1477,164 @@ action.
Scheduled actions
-----------------
-... also known as **crons**...
+**Scheduled actions**, also known as cron jobs, are automated tasks that run periodically at
+predefined intervals. They enable the automation of recurring operations and allow to offload
+compute-intensive tasks to dedicated workers. Scheduled actions are typically used for background
+operations such as data cleanup, third-party synchronization, report generation, and other tasks
+that don't require immediate user interaction.
-.. todo: explain magic commands
-.. todo: ex: 6,0,0 to associate tags to properties in data
+In Odoo, scheduled actions are implemented through the `ir.cron` model. When triggered, they execute
+arbitrary code on a specified model, most commonly by calling a model method that implements the
+desired business logic. Creating a scheduled action is simply a matter of adding a record to
+`ir.cron`, after which a cron worker will execute it at the specified intervals.
+
+.. example::
+ The following example implements a scheduled action that automatically reassigns inactive
+ products or products without sellers to the default seller.
+
+ .. code-block:: xml
+
+
+ Reassign Inactive Products
+
+ model._reassign_inactive_products()
+ 1
+ weeks
+
+
+ .. code-block:: python
+
+ from odoo import api, models
+ from odoo.fields import Command
+
+
+ class Product(models.Model):
+
+ @api.model
+ def _reassign_inactive_products(self):
+ # Clear sellers from underperfoming products.
+ underperforming_products = self.search([('sales_count', '<', 10)])
+ underperforming_products.write({
+ 'seller_ids': [Command.clear()], # Remove all sellers.
+ })
+
+ # Assign the default seller to products without sellers.
+ products_without_sellers = self.search([('seller_ids', '=', False)])
+ if products_without_sellers:
+ default_seller = self.env.ref('product.default_seller')
+ products_without_sellers.write({
+ 'seller_ids': [Command.set(default_seller.ids)] # Replace with default seller.
+ })
+
+ .. note::
+ - The cron is scheduled to run weekly thanks to `interval_number=1` and
+ `interval_type='weeks'`.
+ - The `@api.model` decorator indicates the method operates on the model and records in `self`
+ are not relevant. This serves both as documentation and enables RPC calls without requiring
+ record IDs.
+ - Field commands are required for `One2many` and `Many2many` fields since they cannot be
+ assigned values directly.
+ - `Command.set` takes a list of IDs as argument, which the `ids` recordset attribute
+ conveniently provides.
+
+.. seealso::
+ - Reference documentation on :ref:`scheduled actions `.
+ - Reference documentation on the :meth:`@api.model ` decorator.
+ - Reference documentation on :ref:`field commands `.
+
+.. exercise::
+ #. Create a scheduled action that automatically refuses offers that have expired.
+ #. Create a scheduled action that automatically applies a 10% discount and adds the "Price
+ Reduced" tag to inactive properties. A property is considered inactive if it didn't receive
+ any offers 2 months after it was listed.
+
+ .. tip::
+ To test your crons manually, activate the :doc:`developer mode
+ `, then go to :menuselection:`Settings --> Technical
+ --> Scheduled Actions`, and click :guilabel:`Run Manually` in form view.
+
+.. spoiler:: Solution
+
+ .. code-block:: python
+ :caption: `__manifest__.py`
+ :emphasize-lines: 3
+
+ 'data': [
+ # Model data
+ 'data/ir_cron_data.xml',
+ [...]
+ ],
+
+ .. code-block:: xml
+ :caption: `ir_cron_data.xml`
+
+
+
+
+
+ Real Estate: Discount Inactive Properties
+
+ model._discount_inactive_properties()
+ 1
+ days
+
+
+
+ Real Estate: Refuse Expired Offers
+
+ model._refuse_expired_offers()
+ 1
+ days
+
+
+
+
+ .. code-block:: python
+ :caption: `real_estate_offer.py`
+ :emphasize-lines: 1-4
+
+ @api.model
+ def _refuse_expired_offers(self):
+ expired_offers = self.search([('expiry_date', '<', fields.Date.today())])
+ expired_offers.action_refuse()
+
+ .. code-block:: xml
+ :caption: `real_estate_tag_data.xml`
+ :emphasize-lines: 1-4
+
+
+ Price Reduced
+ 1
+
+
+ .. code-block:: python
+ :caption: `real_estate_property.py`
+ :emphasize-lines: 3,10-24
+
+ from odoo import _, api, fields, models
+ from odoo.exceptions import UserError, ValidationError
+ from odoo.fields import Command
+ from odoo.tools import date_utils
+
+
+ class RealEstateProperty(models.Model):
+ [...]
+
+ @api.model
+ def _discount_inactive_properties(self):
+ two_months_ago = fields.Date.today() - date_utils.relativedelta(months=2)
+ price_reduced_tag = self.env.ref('real_estate.tag_price_reduced')
+ inactive_properties = self.search([
+ ('create_date', '<', two_months_ago),
+ ('active', '=', True),
+ ('state', '=', 'new'),
+ ('tag_ids', 'not in', price_reduced_tag.ids), # Only discount once.
+ ])
+ for property in inactive_properties:
+ property.write({
+ 'selling_price': property.selling_price * 0.9,
+ 'tag_ids': [Command.link(price_reduced_tag.id)],
+ })
----