> For the complete documentation index, see [llms.txt](https://docs.lineverge.com/automail/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lineverge.com/automail/configurations/workflow-configuration/sync-workflows.md).

# Sync Workflows

Sync workflows make use of the generic `do_bulk_sync` function, which was designed to:

* **Generate new Autoform links from Automail.**
* **Update existing Autoform links** by doing a smart merge of its content with what exists in Automail. The merging algorithm compares the Autoform creation timestamp with the update timestamps of each data point to know whether to keep what exists in Autoform or to overwrite with Automail data.
* **Load filled Autoform links back into Automail**. This works for data in form, table, and file formats.
* **Send emails to recipient email addresses** based on a flag logic.

{% hint style="success" %}
**All configurations for a specific workflow must be within the same py file**.&#x20;

For example, all configurations related to the Material Delivery process must be inside autoform\_materialdelivery.py)
{% endhint %}

{% hint style="info" %}
All models used for monitoring the sync process must contain the word **Monitor** contained in their class name and inherit the abstract mixin parent class polygon.apps.app\_autoform.models.**AutoformRequestMixin**.
{% endhint %}

## How to use it?

### Example

<pre class="language-python" data-full-width="true"><code class="lang-python">try:
    from _init import * # DEV env
except ImportError:
    from configurations.workflow_env import * # PROD env
    
from polygon.apps.app_autoform.sync import do_bulk_sync


def get_field_definitions_ecopack_primary_packaging():
    field_definitions_ecopack_primary_packaging = global_variables.field_definitions_ecopack_primary_packaging

    valid_material_list = []
    for cookie_category in custom_models.Cookie.objects.all():
        valid_material_list.append(
            {
                "conditions": [
                    {
                        "function": "eq",
                        "params": {
                            "value": "category__label",
                            "target": cookie_category.label,
                        },
                    },
                    {
                        "function": "in",
                        "params": {
                            "value": "material__name",
                            "list": list(
                                custom_models.CookieMaterial.objects.filter(
                                    material_type=cookie_category.material_type,
                                ).values_list(
                                        "name", 
                                        flat=True,
                                )
                                .distinct()
                            ),
                        },
                    },
                    
                ],
                "value": True,
            },
        )
    
    field_definitions_ecopack_primary_packaging["material__name"]["configured_validations"] = [
        {
            "function": "valid",
            "message": "The selected material is not valid for the packaging type of the same row.",
            "params": {
                "if": valid_material_list,
                "else": {
                    "value": False,
                },
            },
        }
    ]
    return field_definitions_ecopack_primary_packaging

def get_autoform_navs(monitor):
    autoform_navs = []

    def get_hanger_card_elements(card_number):
        card_config = []
        hanger_reference_config = {
            "item_name": "reference",
            "class": "col-6",
            "type": "select",
            "options_lists": "hanger_reference_options_config",
            "attributes": {
                "required": True,
                "data-live-search": "true",
                "title_map": {"en": "Select One..."},
                "focusout": "update_options",
            },
            "related_item_ref": f"hanger_supplier_size_{card_number}",
            "model": custom_models.EcoPackMonitorPrimaryPackaging,
        }
        hanger_supplier_size_config = {
            "item_ref": f"hanger_supplier_size_{card_number}",
            "item_name": "supplier_size",
            "class": "col-6",
            "type": "select",
            "attributes": {
                "required": True,
                "data-live-search": "true",
                "title_map": {"en": "Select One..."},
            },
            "model": custom_models.EcoPackMonitorPrimaryPackaging,
        }
        card_config.append(hanger_reference_config)
        card_config.append(hanger_supplier_size_config)

        return card_config     

    def add_primary_packaging_tab(monitor):
<strong>        def get_hanger_group_children():
</strong>            hanger_children = []
            hanger_children.append(
                {
                    "item_name": "has_hanger",
                    "label_map": {
                        "en": "Packaging that is part of the product&#x3C;br>&#x3C;strong>Is there a referenced hanger?&#x3C;/strong>"
                    },
                    "attributes": {
                        "focusout": "append",
                    },
                    "model": custom_models.EcoPackMonitorPrimaryPackaging,
                    "children": [
                        {
                            "parent_value": True,
                            "category": "card",
                            "class": "mb-3",
                            "title_map": {"en": f"Select your Hanger Configurations"},
                            "description_map": {
                                "en": "Please select in the order of &#x3C;strong>Reference > Supplier (Size)&#x3C;/strong>"
                            },
                            "children": [
                                {
                                    "category": "group",
                                    "class": "card-body mr-0 ml-0 form-row my-0 py-0 border-top pt-3",
                                    "children": get_hanger_card_elements(1),
                                }
                            ],
                            "model": custom_models.EcoPackMonitorPrimaryPackaging,
                            "load_method": {
                                "function": "upsert",
                                "params": {
                                    "key_fields": {
                                        "ecopackmonitor_id": lambda monitor: monitor.id,
                                    },
                                    "defaults": {
                                        "reference": {
                                            "function": "get_child_item_value",
                                        },
                                        "supplier_size": {
                                            "function": "get_child_item_value",
                                        },
                                    },
                                },
                            },
                        },
                    ],
                },
            )
            return hanger_children

        autoform_navs.append(
            {
                "label_map": {"en": "Primary Packaging"},
                "children": [
                    {
                        "item_ref": "g_hanger_append",
                        "category": "group",
                        "class": "group_to_append",
                        "children": get_hanger_group_children(),
                    },
                    {
                        "item_name": "primary_packaging",
                        "label_map": {
                            "en": """
                                &#x3C;strong>Instructions&#x3C;/strong>
                                &#x3C;ul>
                                    &#x3C;li>&#x3C;small>Please note that you should declare your hanger (the plastic hanger and the metal hook if there is) in this table ONLY if your hanger ref + supplier is not present in the above hanger section &#x3C;/small>&#x3C;/li>
                                    &#x3C;li>&#x3C;small>If there is an inner polybag (inner packing), DO NOT declare it in the table below&#x3C;/small>&#x3C;/li>
                                &#x3C;/ul>
                                &#x3C;strong>Declare OTHER Primary Packaging Categories in the Table Below (like hangtags, plastic ties etc. )&#x3C;/strong>
                            """
                        },
                        "attributes": {"required": True},
                        "table_props": {
                            "model": custom_models.EcoPackForm,
                            "queryset": custom_models.EcoPackForm.objects.filter(
                                ecopackmonitor=monitor
                            ),
                            "field_definitions": get_field_definitions_ecopack_primary_packaging(),
                            "target_rows": 20,
                            "table_settings": {
                                "permissions": {
                                    "delete": {
                                        "allowed": True,
                                    }
                                },
                            },
                        },
                    },
                ],
            },
        )

    def add_secondary_packaging_tab():
        def get_secondary_packaging_questions():
            questions = []
            questions.append(
                {
                    "item_name": "has_inner_polybag",
                    "label_map": {
                        "en": "Secondary packaging is the packaging of the master &#x26; inner cartons&#x3C;br>&#x3C;strong>Do you use an inner polybag (not master carton polybag) for the SPCB &#x26; inner packing?&#x3C;/strong>"
                    },
                    "model": custom_models.EcoPackMonitorSecondaryPackaging,
                }
            )
            questions.append(
                {
                    "item_name": "master_carton_weight",
                    "model": custom_models.EcoPackMonitorSecondaryPackaging,
                    "type": "text",
                    "validations": {
                        "datatype": "float",
                        "numeric_filter": {
                            "gte": 50,
                            "lte": 50000,
                        },
                    },
                    "helper_text_map": {"en": "Numbers only"},
                }
            )
            return questions

        autoform_navs.append(
            {
                "item_name": "secondary_packaging",
                "label_map": {"en": "Secondary Packaging"},
                "children": [
                    {
                        "category": "group",
                        "children": get_secondary_packaging_questions(),
                        "model": custom_models.EcoPackMonitorSecondaryPackaging,
                        "load_method": {
                            "function": "upsert",
                            "params": {
                                "key_fields": {
                                    "ecopackmonitor_id": lambda monitor: monitor.id,
                                },
                                "defaults": {
                                    "has_inner_polybag": {
                                        "function": "get_child_item_value",
                                    },
                                    "master_carton_weight": {
                                        "function": "get_child_item_value",
                                    },
                                },
                            },
                        },
                    },
                ],
            }
        )

    add_primary_packaging_tab(monitor)
    add_secondary_packaging_tab()
    # etc.

    return autoform_navs


if __name__ == "__main__": # Check if script is being run directly and execute the following block if true

    do_bulk_sync(
        params={
            "queryset": custom_models.Monitor.objects.all().order_by("id"), # queryset always on the Monitor model and used for both push and pull!
            "language": lambda monitor: monitor.autoform_language if monitor.autoform_language else "en",
            "sender_email": settings.EMAIL_USERNAME_SEND,
<strong>            "workflows": {
</strong>                "push": {
                    "keep_wip": True, # powerful feature to keep the work in progress (WIP) of the Autoform
                    "conditions": {
                        "start": lambda monitor.is_in_queue,
                        "refresh": lambda monitor: monitor.autoform_hash is None or monitor.to_be_refreshed,
                        "email": True, # high-level condition to control whether any email is sent (email-specific conditions are defined at email level below)
                    },
                    "emails": {
                        "initial_email": {
                            "condition": lambda monitor: monitor.sent is False,
                            "email_receivers": {
                                "to": get_email_receivers,
                                "cc": get_email_receivers_additional_cc,
                            },
                            "email_content": {
                                "subject": {
                                    "en": lambda monitor: f"{settings.COMPANY_NAME} {monitor.suppliergroup.supplier_name} - {monitor.season} - {monitor.product.product_code} Cookie",
                                },
                                "body": {
                                    "en": lambda monitor: f"""
                                        &#x3C;p>Dear &#x3C;strong>{monitor.suppliergroup.supplier_name}&#x3C;/strong>,&#x3C;/p>
                                        &#x3C;br />
                                        &#x3C;p>You are expected to provide the information for your primary, secondary and tertiary packaging for reference &#x3C;strong>{monitor.product.product_code}&#x3C;/strong> of &#x3C;strong>{monitor.season}&#x3C;/strong>.&#x3C;/p>
                                        &#x3C;br /> 
                                            &#x3C;p>Please click on the link to open the form&#x3C;/p>

                                            &#x3C;p>{monitor.autoform_link_button}&#x3C;/p>
                                        &#x3C;br /> 
                                        &#x3C;p>We are expecting the form to be submitted before &#x3C;strong style="color:#ff6384">{monitor.deadline_verbose}&#x3C;/strong>.&#x3C;/p>
                                        &#x3C;br />
                                        
                                        &#x3C;p>Thanks for your cooperation.&#x3C;/p>
                                        &#x3C;br />
                                        
                                        &#x3C;p>Best Regards,&#x3C;/p>
                                        &#x3C;p>Sourcing Team&#x3C;/p>
                                        &#x3C;p>{settings.COMPANY_NAME}&#x3C;/p>
                                    """,
                                },
                            },
                            "procedures": [
                                lambda monitor: setattr(
                                    monitor, "sent", True
                                ),
                                set_sent_timestamp,
                                lambda monitor: setattr(
                                    monitor,
                                    "status",
                                    "Pending",
                                ),
                            ],
                        },
                        "reminder_email": {
                            "condition": lambda monitor: monitor.reminder_sent is False,
                            "email_receivers": {
                                "to": get_email_receivers,
                                "cc": get_email_receivers_additional_cc,
                            },
                            "email_content": {
                                "subject": {
                                    "en": lambda monitor: f"{monitor.suppliergroup.supplier_name} - {monitor.season} - {monitor.product.product_code} Cookie Reminder",
                                },
                                "body": {
                                    "en": lambda monitor: f"""
                                        &#x3C;p>Dear &#x3C;strong>{monitor.suppliergroup.supplier_name}&#x3C;/strong>,&#x3C;/p>
                                        &#x3C;br />
                                        &#x3C;p>You are reminded to provide the information for your primary, secondary and tertiary packaging for reference &#x3C;strong>{monitor.product.product_code}&#x3C;/strong> of &#x3C;strong>{monitor.season}&#x3C;/strong>.&#x3C;/p>
                                        &#x3C;br /> 
                                            &#x3C;p>Please click on the link to open the form&#x3C;/p>

                                            &#x3C;p>{monitor.autoform_link_button}&#x3C;/p>
                                        &#x3C;br /> 
                                        &#x3C;p>We are expecting the form to be submitted before &#x3C;strong style="color:#ff6384">{monitor.deadline_verbose}&#x3C;/strong>.&#x3C;/p>
                                        &#x3C;br />
                                        
                                        &#x3C;p>Thank you for your cooperation.&#x3C;/p>
                                        &#x3C;br />
                                        
                                        &#x3C;p>Best Regards,&#x3C;/p>
                                        &#x3C;p>Sourcing Team&#x3C;/p>
                                        &#x3C;p>{settings.COMPANY_NAME}&#x3C;/p>
                                    """,
                                },
                            },
                            "procedures": [
                                lambda monitor: setattr(
                                    monitor, "reminder_sent ", False
                                ),
                                set_sent_timestamp,
                            ],
                        },
                    },
                    "procedures": {
                        "before_refresh": set_deadline,
                        "during_refresh": translate_autoform_config,
                        "after_refresh": [
                            lambda monitor: setattr(
                                monitor, "to_be_refreshed", False
                            ),
                            lambda monitor: setattr(monitor, "autoform_loaded", False),
                        ],
                        "before_email": set_monitor_email_variables,
                        "after_email": [
                            lambda monitor: setattr(
                                monitor,
                                "email_description_prefix",
                                "Email sent to",
                            ),
                            create_ecopackmonitorevent,
                        ],
                        "before_save": lambda monitor: setattr(
                            monitor, "is_in_queue", False
                        ),
                        "after_save": None,
                    },
                },
                "pull": {
                    "conditions": {
                        "start": True,
                        "load": True,
                        "refresh": True, # to refresh the Autoform link right after loading
                        "opened_check": False,
                        "email": False,
                    },
                    "emails": {
                        "success": {
                            "condition": lambda monitor: monitor.autoform_loaded is True
                            and monitor.success_email_sent is False,
                            "email_content": {
                                "subject": {
                                    "en": lambda monitor: f"Successful Submission of {monitor.season.name} Cookie Form - {monitor.suppliergroup.supplier_name} (ref:{monitor.product.product_code})",
                                },
                                "body": {
                                    "en": lambda monitor: f"""
                                        &#x3C;p>Dear {monitor.suppliergroup.supplier_name},&#x3C;/p>

                                        &#x3C;p>Thank you for you submission.&#x3C;/p>

                                        &#x3C;p>
                                            We have received the &#x3C;strong>{monitor.season}&#x3C;/strong> &#x3C;strong>Cookie Form&#x3C;/strong> for reference &#x3C;strong>{monitor.product.product_code}&#x3C;/strong>
                                            on &#x3C;u>{monitor.sent_d}&#x3C;/u> at &#x3C;u>{monitor.sent_t}&#x3C;/u> ({settings.TIME_ZONE} time)
                                            for the &#x3C;strong>{monitor.period}&#x3C;/strong>.
                                        &#x3C;/p>

                                        &#x3C;p>{monitor.autoform_link_button_print}&#x3C;/p>

                                        &#x3C;p style="color:#36ebaa;">
                                            No further action is required.
                                        &#x3C;/p>

                                        &#x3C;p>Thank you!&#x3C;/p>
                                    """,
                                },
                            },
                        }
                    },
                    "procedures": {
                        "before_load": None,
                        "during_load_label_value_list": None,
                        "during_load_table": None,
                        "after_load": [
                            lambda monitor: setattr(monitor, "autoform_loaded", True),
                            lambda monitor: setattr(monitor, "is_in_queue", True),
                            lambda monitor: setattr(
                                monitor,
                                "status",
                                "Accepted",
                            ),
                        ],
                        "before_email": None,
                        "after_email": [
                            lambda monitor: setattr(
                                monitor,
                                "email_description_prefix",
                                "Success email sent to",
                            ),
                            create_ecopackmonitorevent,
                        ],
                        "before_opened_check": None,
                        "after_opened_check": [
                            lambda monitor: setattr(
                                monitor,
                                "autoform_opened",
                                True,
                            ),
                            lambda monitor: setattr(
                                monitor,
                                "status",
                                "Link Opened",
                            ),
                        ],
                        "before_save": [
                            set_monitor_status_and_stats,
                        ],
                        "after_save": None,
                    },
                },
            },
            "autoform_config": {
                "autoform_frame": {
                    "chat": {
                        "active": True,
                        "application": "automail-lv-cookie",
                        "module": "Cookie Module",
                        "context": "This is for suppliers to declare all the packaging they have used in the 1 specified product.",
                    },
                    "stage": "prod",
                    "keep_open": True,
                    "grid": "neat",
                    "logo": "static/uploads/Lineverge/logo.png",
                    "logo_width": "120px",
                    "title_map": {
                        "en": "Cookie", 
                    },
                    "subtitle": lambda monitor: f"""
                        &#x3C;strong>{monitor.suppliergroup.supplier_name}&#x3C;/strong>&#x3C;br />
                        {monitor.season} - {monitor.product.product_code}
                    """,
                    "description_map": {"en": f"Please select all the items you use in your packaging."},
                    "successful_submit_subtitle_map": {
                        "en": "Your answers have been submitted, the form will stay open in case you need to make further changes. &#x3C;br/>&#x3C;br/>Redirecting in 10 seconds... &#x3C;meta http-equiv='refresh' content='10'>",
                    },
                    "successful_submit_description_map": {
                        "en": f"For questions or support please contact your contact point.",
                    },
                    "languages": [
                        {"code": "en", "label": "English"},
                        {"code": "cn", "label": "简体中文"},
                        {"code": "tr", "label": "Türk"},
                    ],
                    "downloads": [],
                },
                "autoform_lists": {
                    "hanger_reference_options_config": get_hanger_reference_options_config(),
                },
                "autoform_navs": get_autoform_navs,
            },
        },
    )
</code></pre>

### Important design considerations

`do_bulk_sync` has 1 input parameter **params**, which is a dictionary.

The sync feature has a validation function that will prevent `do_bulk_sync` from running if it detects anomalies (i.e. the execution is exited through asserts).

The value of each key and sub-key in params can either be any non-callable Python object (e.g. int, str, dict, list, etc.) or a function.&#x20;

All functions must have the input parameter **monitor**, which refers to the current monitor row from the queryset.&#x20;

{% hint style="info" %}
Lambda functions are short, inline, unnamed functions defined using the <mark style="color:purple;">`lambda`</mark> keyword, whereas regular functions are defined using the <mark style="color:purple;">`def`</mark> keyword and can have multiple statements.
{% endhint %}

For simple cases such as getting or setting a monitor field, use an inline Python <mark style="color:purple;">lambda</mark> function. For more complex cases, use regular Python functions.

In most cases, it is better to have your custom functions executed within sync. To do so, pass the function as a parameter without compiling it in the py file.

In the example code, `get_hanger_reference_options_config` will execute directly in the py file as the function is provided using the parenthesis:

```
"hanger_reference_options_config": get_hanger_reference_options_config(),
```

Whereas in the following code, `get_hanger_reference_options_config` would be executed later in the sync as the function is provided without the parenthesis:

```
"hanger_reference_options_config": get_hanger_reference_options_config,
```

`get_hanger_reference_options_config` would then be defined within the py file with the input param monitor:

```
def get_hanger_reference_options_config(monitor):
    hanger_reference_list = [
        value for value in list(
            custom_models.Hanger.objects.filter(material__material_type="PLASTIC")
            .values_list("reference", flat=True)
            .distinct()
        )
    ]
    hanger_reference_options_config = []
    for reference in hanger_reference_list:
        option = {
            "item_value": reference,
            "label_map": {"en": reference},
            "options": [
                {"item_value": value, "label": value}
                for value in custom_models.Hanger.objects.filter(reference=reference, material__material_type="PLASTIC")
                .values_list("search_reference_suggestion_text", flat=True)
                .distinct()
            ],
        }
        hanger_reference_options_config.append(option)
    return hanger_reference_options_config
```

{% hint style="info" %}
**monitor** refers to a given row of queryset. The input parameter of any provided function would always be **monitor** even if the model was called MonitorEcoPack.

If additional variables are needed within the workflow, you can add them to the **monitor** object even if they are not fields defined in the corresponding Monitor model. This is done in the example above for `deadline_verbose`, which is set in set\_monitor\_email\_variables to be used as email variables, but it is not a field in **monitor**.
{% endhint %}

{% content-ref url="/pages/pnBXKMW4cr3Es8TAwiWs" %}
[Autoform Nav Elements](/automail/configurations/workflow-configuration/sync-workflows/autoform-nav-elements.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.lineverge.com/automail/configurations/workflow-configuration/sync-workflows.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
