扩展自动化

应用可以通过三种方式扩展自动化流程:发出事件,让流程据此触发或等待;添加操作,加入步骤列表;以及添加关系,让步骤能够访问架构未直接关联的记录。每一种都在应用的 hooks.py 中注册。

发出事件

在应用中发生有意义的事情时,调用 emit():

from frappe.automation_engine import emit

def on_deal_won(deal):
    emit("crm.deal_won", doc=deal, payload={"amount": deal.deal_value})
参数 说明
event 事件名称。使用带应用前缀的点分名称,例如 crm.deal_won。
doc 事件所涉及的单据。带有文档类型的自定义事件流程会针对它运行。
payload 一个可 JSON 序列化的字典。流程在模板中通过 payload 读取它,在条件中通过 context["payload"] 读取它。
correlation_key 恢复暂停在匹配的 WaitForEvent 步骤上的运行。

emit() 做两件事:

  1. 它为该事件排队所有已启用的自定义事件流程。
  2. 如果提供了 correlation_key,它会恢复任何使用相同键等待该事件的运行。

它返回 {"queued": <count>, "resumed": <count>}</count></count>。运行会在当前事务提交后开始,在此之前对同一记录调用两次 emit() 只会产生一次运行。超过自动化设置中事件负载限制(默认 64 KB)的负载会被拒绝。

恢复等待中的流程

流程可以先发送一封邮件,然后用关联键 {{ doc.name }} 执行 WaitForEvent crm.email_replied。当回复到达时,你的应用用相同的键发出相同的事件:

emit(
    "crm.email_replied",
    doc=lead,
    payload={"communication": comm.name},
    correlation_key=lead.name,
)

等待中的运行会立即恢复,其后续步骤可以从 context["event"]["payload"] 读取负载。

注册事件

emit() 只接受已安装应用注册过的事件,除非在自动化设置中开启了允许未注册事件。在 hooks.py 中注册事件,可以是裸名称,也可以带有构建器界面可以显示的元数据:

automation_events = [
    "crm.deal_won",
    {
        "crm.email_replied": {
            "label": "Email replied",
            "description": "A contact replied to an email sent from CRM",
            "subject": {"doctype_key": "reference_doctype", "name_key": "reference_name", "doctypes": ["CRM Lead", "CRM Deal"]},
        }
    },
]

subject 告诉引擎事件涉及哪条记录,从负载中读取。有了主体,CRM Lead 上的流程会针对负载中指定的线索运行,即使发出事件的代码当时持有的是 Communication。当主体类型固定时,用 "doctype": "CRM Lead" 代替 doctype_key。

添加操作

继承 AutomationAction 并实现 execute:

# my_app/automation.py
from typing import ClassVar

import frappe
from frappe.automation_engine.actions.base import AutomationAction, AutomationParamError, render_value

class PostToChannel(AutomationAction):
    action_type = "PostToChannel"
    label = "Post to Channel"
    description = "Post a message to a team channel."
    transactional = False  # it calls an external service
    params_schema: ClassVar[list] = [
        {"fieldname": "channel", "label": "Channel", "fieldtype": "Data", "reqd": 1},
        {"fieldname": "message", "label": "Message", "fieldtype": "Small Text", "reqd": 1},
    ]

    def validate(self, params, doctype):
        if not params.get("channel"):
            raise AutomationParamError("Channel is required", fieldname="channel")

    def execute(self, doc, params, context):
        message = render_value(params["message"], doc, context)
        post_message(params["channel"], message)
        return f"Posted to {params['channel']}"
# hooks.py
automation_actions = ["my_app.automation.PostToChannel"]

类属性:

属性 用途
action_type 存储在步骤上的唯一标识符。发布后不要更改。
label、description 显示在操作类型列表和构建器界面中。
params_schema 参数字段:fieldname、label、fieldtype、options、reqd。
requires_document 如果操作可以在没有触发单据的情况下运行(例如在计划流程上),则设为 False。
applicable_doctypes 将操作限制为这些 DocType。None 表示全部。
supported_trigger_types 将操作限制为这些触发类型。None 表示全部。
transactional 如果操作具有数据库回滚无法撤销的副作用(例如 HTTP 调用或消息),则设为 False。这样引擎就永远不会从头重放该运行。

方法:

  • validate(params, doctype):在流程保存时运行,并在每次执行前再次运行。抛出带有 fieldname 的 AutomationParamError 来指向错误的参数。
  • execute(doc, params, context):执行实际工作。返回一个短字符串用于运行日志,或返回一个包含 detail 键的字典,以及后续步骤应通过 context["steps"][step_key] 读取的任何输出。
  • output_doctype(params):如果操作创建了一条记录,返回其 DocType。这样步骤的输出别名就可以指向新记录,后续步骤也会针对它进行验证。

操作以流程的运行身份用户运行,因此正常的权限检查会生效。不要调用 frappe.db.commit();事务由引擎管理。要从操作内部暂停运行,请抛出 StopAutomation("reason", resume_after=seconds)。

添加关系

Link、Dynamic Link 和子表链接字段会自动成为双向关系。只有当架构无法表达某种连接时(例如通过电子邮件地址匹配的记录),才需要注册提供程序:

from frappe.automation_engine.relationships import AutomationRelationshipProvider

class ContactRelationships(AutomationRelationshipProvider):
    def get_definitions(self, source_doctype):
        if source_doctype != "Communication":
            return []
        return [{
            "name": "sender_lead",
            "label": "Lead (by sender email)",
            "target_doctype": "CRM Lead",
            "cardinality": "one",
        }]

    def resolve(self, source_doc, relationship, params):
        name = frappe.db.get_value("CRM Lead", {"email": source_doc.sender})
        return [{"doctype": "CRM Lead", "name": name}] if name else []
# hooks.py
automation_relationships = ["my_app.automation.ContactRelationships"]
automation_relationship_ignore = ["My Log DocType"]
  • cardinality 是 one 或 many。只有 one 关系可以用作步骤的目标别名。many 关系用于相关记录条件,此时覆盖 query() 以在 SQL 中过滤可以让它们在大表上保持高效。
  • 与派生关系具有相同 name 的定义会替换它,这允许应用重新标记架构链接。
  • 提供程序返回的每个引用在步骤使用之前都会检查其存在性和读取权限。
  • automation_relationship_ignore 会从派生关系中隐藏 DocTypes(例如日志)。

抑制和检查自动化

from frappe.automation_engine import is_enabled, skip_automations

if is_enabled():
    ...

with skip_automations():
    doc.save()  # no Automation Flow fires for this save

步骤与操作

自动化流程中的操作表按从上到下的顺序列出了每次运行所执行的步骤。每一行都有一个步骤类型,对于操作步骤,还有一个操作类型及其参数。

步骤类型

步骤类型 作用
操作 执行一个操作,例如设置字段或发送邮件
等待 将运行暂停一段固定时间,然后继续执行下一步
等待事件 将运行暂停,直到某个应用发出匹配的事件,或超时
条件判断 评估一个条件,只运行匹配分支中的步骤

等待

将参数设置为一个时长:

{"value": 2, "unit": "Days"}

unit 是 Seconds、Minutes、Hours 或 Days 之一。暂停期间,运行显示为等待中。恢复时,文档会重新加载,因此后续步骤能看到等待期间所做的任何更改。

等待事件

暂停直到某个应用发出带有匹配关联键的事件:

{
  "event_name": "crm.email_replied",
  "correlation_key": "{{ doc.name }}",
  "timeout_value": 3,
  "timeout_unit": "Days"
}

关联键在到达该步骤时渲染,当应用以相同的事件和键调用 emit() 时,运行会恢复。如果超时先到,运行也会恢复。等待之后的步骤可以从 context.event 读取它是如何结束的:outcome 是 Matched 或 Timed Out,而 payload 保存事件的数据。

一个典型用例:只有在客户未回复时才发送跟进,方法是在等待之后添加一个条件判断步骤,条件为 context["event"]["outcome"] == "Timed Out"。

条件判断

条件判断步骤需要一个步骤条件。属于它的步骤要将父步骤设置为该条件判断行的编号,并将分支设置为 If(条件为真时运行)或 Else(条件为假时运行)。没有父步骤的步骤始终运行。

# 步骤类型 操作 父步骤 分支
1 条件判断 条件 doc.grand_total > 10000
2 操作 分配给用户:manager@example.com 1 条件为真
3 操作 分配给用户:sales@example.com 1 条件为假
4 操作 发送通知

条件判断步骤可以嵌套。分支在运行首次到达条件判断步骤时决定一次,之后在等待之后不会重新评估。

步骤条件

任何步骤都可以有一个步骤条件,这是一个 Python 表达式,必须为真该步骤才会运行。否则该步骤会被记录为已跳过,运行继续。该表达式可以读取:

  • doc:触发运行的文档
  • target:该步骤所操作的记录
  • context:运行的状态,包括 context["steps"],即按步骤键索引的先前步骤的输出

操作

这些是作为核心框架操作提供的操作。其他应用可以通过 hooks.py 扩展这些操作,参见扩展自动化。

操作类型 作用 关键参数
设置字段值 在目标上设置一个或多个字段并保存 field + value,或作为映射的 values
递增字段值 向数值字段加一个数 field、amount(负数表示减去)
创建文档 插入任意类型的新文档 doctype、values
发送通知 发送邮件或系统通知 channel(Email 或 System)、recipients、subject 和 message,或 email_template
分配给用户 将目标分配给一个或多个用户 assign_to、description
调用 Webhook 发送 HTTP 请求 url、method、headers、payload、timeout
运行脚本 运行 Python 代码 server_script(一个 API 服务器脚本),或内联 script

示例

一次设置两个字段:

{"values": {"status": "Replied", "priority": "High"}}

创建一个跟进待办事项:

{
  "doctype": "ToDo",
  "values": {
    "description": "Call {{ doc.lead_name }}",
    "reference_type": "{{ doc.doctype }}",
    "reference_name": "{{ doc.name }}",
    "allocated_to": "{{ doc.lead_owner }}"
  }
}

给文档所有者和被分配人发送邮件:

{
  "channel": "Email",
  "recipients": ["@owner", "@assignees", "team@example.com"],
  "subject": "{{ doc.name }} was closed",
  "message": "
<p>{{ doc.name }} was closed on {{ frappe.utils.nowdate() }}.</p>"
}

@owner 和 @assignees 在步骤运行时针对文档解析。它们读取的是运行开始时(或最后一次等待之后)加载的文档,因此同一次运行中先前步骤所做的分配还不会包含在内。

模板

文本参数接受 Jinja。可用变量:

  • doc / target:该步骤所操作的记录
  • trigger:启动本次运行的文档
  • payload:来自自定义事件或计划任务的数据
  • context:本次运行的状态,包括用于存放前面步骤输出的 context.steps.<step_key></step_key>

对其他记录执行操作

默认情况下,步骤作用于触发文档。若要对关联记录执行操作,请在 Relationships 下声明一个命名别名,并将步骤的 Target 设置为该别名:

[{"alias": "customer", "relationship": "customer"}]

Link、Dynamic Link 和子表链接字段会自动以其字段名提供。反向链接命名为 <doctype>_via_<field></field></doctype>,例如 crm_deal_via_lead。应用可以注册更多(参见 Extending Automations)。

Create Document 步骤还可以设置 Output Alias。之后的步骤便可以以它创建的文档为目标。

错误处理

  • Stop on Error(默认开启):第一个失败的步骤会停止本次运行,运行被标记为 Failed。关闭它可继续执行;此时运行被标记为 Partially Failed。
  • 每个步骤都在自己的保存点内运行,因此失败步骤的数据库更改会被回滚,而不会撤销之前的步骤。
  • Call Webhook 和 Run Script 可以访问数据库外部。一旦其中之一运行过,本次运行就绝不会从头重放,因此 webhook 不会被发送两次。
  • Call Webhook 会拒绝私有和内部网络地址,并且最多跟随五次重定向。响应状态为 400 或以上会导致该步骤失败。
  • Run Script 需要启用 Server Scripts(bench set-config -g server_script_enabled 1),并且只有 System Manager 才能编写内联脚本。脚本可以读取 doc、target、trigger、payload 和 context,并可以在 result 上设置键,以将输出发布给后续步骤。它不能调用 frappe.db.commit()。

测试运行

在已保存的流程上点击 Test Run,然后选择一个文档。流程会针对它真实运行,随后 所有更改都会被回滚。结果会列出每个步骤的状态、消息以及它读取到的条件值。

在测试运行期间,Wait 步骤会被模拟,Call Webhook 会报告它将会发送的请求,但不会实际发送。

触发器和条件

触发器决定自动化流程何时被考虑。匹配字段和条件决定某个特定文档是否符合条件。两者都在自动化流程表单上设置。

触发器类型

触发器类型 触发时机 所需配置
文档创建 插入文档时 文档类型
文档更新 保存已有文档时(插入时不触发) 文档类型
字段值变更 保存时某个字段的值发生变化 文档类型、触发字段
文档删除 删除文档时 文档类型
文档提交 提交可提交的文档时 文档类型
文档取消 取消可提交的文档时 文档类型
基于日期 日期字段在今天的 N 天前或 N 天后 文档类型、日期字段
定时调度 cron 表达式到期时 Cron 表达式
自定义事件 应用调用 frappe.automation_engine.emit() 时 自定义事件名称
手动 有人显式启动流程时 —

字段值变更

将触发字段设置为要监视的字段。可选地通过以下方式缩小范围:

  • 原值:仅当旧值为该值时触发。
  • 新值:仅当新值为该值时触发。

任一项留空表示接受任意值。例如,触发字段为 status,新值为 Closed,则每当文档被关闭时触发,无论其之前的状态是什么。

基于日期

选择一个日期字段、一个日期偏移(天)和一个日期方向:

  • 3 天之前 renewal_date:对续约日期距今三天的文档触发。
  • 1 天之后 due_date:对昨天变为逾期的文档触发。

基于日期的流程每小时检查一次。无论检查运行多少次,每个文档每天最多触发一次。

定时调度

输入标准的五字段Cron 表达式,例如 0 9 * * 1 表示每周一 09:00。保存后表单会显示下次运行时间。

  • 不设置文档类型时,流程按调度运行一次,不涉及文档。适用于周期性任务,如每周摘要邮件或调用外部 API。
  • 设置文档类型时,每次调度到期时,流程会为每个匹配其匹配字段和条件的文档各运行一次。

如果调度器宕机错过了多次触发,流程只会为最近一次补运行一次,而不是为每次错过的触发都补运行。

自定义事件

自定义事件流程在应用发出命名事件时运行,例如 crm.deal_won。在自定义事件中按应用发出的事件名称准确输入。应用如何发出和注册事件在扩展自动化中介绍。

如果设置了文档类型,流程仅对该类型文档相关的事件运行。

手动

手动流程不会自行触发。仅当应用通过 frappe.automation_engine.api.run_manually 方法启动它时才运行,例如从应用 UI 中的按钮启动。调用者需要对流程有写权限,对文档有读权限。

匹配字段

匹配字段使用与列表视图相同的筛选编辑器。每个字段规则添加一行。文档必须满足所有规则,流程才会运行。

例如,在一个线索流程中:

  • 状态 = Open
  • 来源已设置
  • 年收入 > 1000000

没有匹配字段的流程会匹配其类型的所有文档。

高级条件

对于匹配字段无法表达的逻辑,在使用 Python 的高级表达式中编写 Python 表达式。文档可通过 doc 访问,表达式使用 Frappe 的安全求值器求值,因此可以使用 frappe.utils 辅助函数。

doc.status == "Open" or doc.priority == "High"
doc.grand_total > 50000 and doc.customer_group != "Internal"

匹配字段和表达式必须同时为真。如果表达式抛出错误,文档将被视为不匹配,错误会写入错误日志。

运行时重新验证

触发器在文档保存时匹配,但步骤稍后在后台工作进程中运行。勾选运行时重新验证可在执行前再次测试匹配字段和条件。如果文档不再匹配,该次运行将记录为已跳过。

当文档在触发流程的保存之后可能快速变化时(例如状态被设置后又还原),这很有用。

运行身份

运行身份决定步骤以谁的权限运行:

  • 自动化用户(默认):自动化用户字段中的用户,除非你更改它,否则为 Administrator。
  • 触发用户:其操作触发了流程的用户。
  • 文档所有者:流程所针对的文档的所有者。

每个步骤仍以该用户的身份运行正常的权限检查。如果某个步骤因权限错误而失败,请检查“运行身份”用户是否能够读取触发文档并写入该步骤所更改的记录。

当流程未触发时

在安装、迁移和补丁期间,当自动化设置中勾选了禁用自动化时,以及在 site_config.json 中设置了 automation_disabled 时,自动化流程会被跳过。

必须在不触发流程的情况下保存文档的代码(例如数据导入)可以抑制流程:

from frappe.automation_engine import skip_automations

with skip_automations():
    doc.save()

自动化

自动化流程的意思是“当这类记录发生这件事时,按顺序执行这些操作”,无需编写代码。你选择一个触发器,用条件缩小范围,然后添加一系列步骤:更新字段、发送通知、分配人员、调用 webhook、等待两天、根据某个值分支。

何时使用自动化流程

Frappe 已经为常见任务提供了单一用途的工具,当其中某个工具正好满足你的需求时,它仍然是正确的选择:

你想要… 使用
在某个事件发生时发送一封邮件或系统通知 通知
在某个事件发生时将文档提交到外部 URL Webhook
在团队中分配新文档 分配规则
用代码更改文档的行为 服务器脚本

当你需要这些工具单独无法完成的事情时,请使用自动化流程:

  • 按顺序执行多个步骤:分配文档,然后给被分配人发邮件,然后创建一个后续任务。
  • 等待:做某件事,等待两天,然后再做另一件事,或者等到另一个事件发生。
  • 分支:如果交易金额超过 10,000,就做一件事,否则做另一件事。
  • 访问关联记录:触发器是一条 Communication,但你想更新的记录是它所属的 Lead。

工作原理

  1. 触发器:文档事件、日期、计划、来自应用的自定义事件或手动运行会启动流程。请参阅触发器和条件。
  2. 匹配:流程的匹配字段和条件决定此文档是否符合条件。
  3. 排队:匹配的触发器会被排队,而不是内联执行。后台工作进程会在请求提交后不久处理它,因此缓慢的 webhook 永远不会阻塞用户的保存操作。在工作进程运行之前,对同一文档的重复保存会合并为一次运行。
  4. 运行:工作进程按顺序执行步骤,并写入一条自动化运行记录,其中包含每个步骤的结果。请参阅步骤和操作。

由于运行发生在后台,因此调度器和后台工作进程必须正在运行,自动化才会触发。

创建流程

  1. 在 awesome bar 中转到自动化流程,然后点击新建。
  2. 输入标题,并选择流程所涉及的文档类型。
  3. 选择触发器类型,并填写它要求的字段。
  4. 可选地添加匹配字段和使用 Python 的高级表达式。
  5. 在操作下添加一行或多行。
  6. 保存并点击测试运行,针对真实文档进行尝试(所有更改都会回滚),然后勾选已启用并再次保存。

流程只有在至少有一个操作时才能启用。

示例:跟进高价值线索

  • 文档类型:Lead
  • 触发器类型:文档创建
  • 匹配字段:年收入 > 1,000,000
  • 操作:
    1. 分配给用户:sales-head@example.com
    2. 发送通知:电子邮件发送至 sales-head@example.com,主题为 New lead: {{ doc.lead_name }}
    3. 等待:2 天
    4. 发送通知:发送邮件至 @assignees,主题为 Follow up on {{ doc.lead_name }},并附带一个步骤条件 doc.status == "Lead",这样如果已经有人推进了该线索,就会跳过此步骤。

运行历史

每次运行都会保存为一条自动化运行记录,该记录会关联到对应的流程以及它所运行的文档。每次运行都有一个状态:

  • 成功:每个步骤都成功执行或被跳过。
  • 部分失败:某个步骤失败,且出错时停止处于关闭状态,因此后续步骤仍会继续运行。
  • 失败:某个步骤失败,且 Stop on Error 停止了运行,或者运行无法启动。
  • 等待中:运行在 Wait step 处暂停,并会自动恢复。
  • 已跳过:当 Revalidate on Run 开启时,文档已被删除或不再匹配。

打开一次运行,可以查看每个步骤的状态、输出、耗时,对于失败的步骤,还可以查看错误信息和堆栈跟踪。对于被跳过的步骤,运行记录会显示条件及其读取到的值,这样你就能看出它为什么没有匹配。运行日志会按照标准日志清理机制在 30 天后清除。

自动化设置

自动化设置包含站点范围的开关和限制:

  • 禁用自动化:停止在站点上派发所有流程。已排队的记录不受影响。若要在数据库不可达时也停止自动化,请在 site_config.json 中设置 "automation_disabled": 1。
  • 最大递归深度(默认 3):一个流程的操作可以触发另一个流程多少次,超过后该链条将被拒绝。这可以防止两个相互更新的流程无限循环。
  • 失败阈值(默认 10):在连续这么多次运行失败后,该流程会被自动禁用,其待处理的运行会被跳过,并且其所有者会收到通知。原因会显示在该流程的禁用原因字段中。
  • 允许未注册事件:接受没有任何已安装应用注册的自定义事件。请参阅扩展自动化。

其余设置(排空时机、重试、保留期限和大小限制)用于调整后台工作进程,通常很少需要更改。

后续步骤

  • 触发器与条件
  • 步骤与操作
  • 扩展自动化(面向应用开发者)