Fathom 会议摘要生成器到 Dart 文档和任务

高级

这是一个Document Extraction、AI Summarization领域的自动化工作流,包含 16 个节点。主要使用 Code、Dart、Webhook、Agent、LmChatOpenAi 等节点。 从 Fathom 录音生成 AI 会议摘要和任务到 Dart

前置要求
  • HTTP Webhook 端点(n8n 会自动生成)
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "pCErB9iQNvcrHjuD",
  "meta": {
    "instanceId": "716c5c2f015e2546f9bfe101c02ffa34feec3b9c7c53614feecf94b0d705db79",
    "templateCredsSetupCompleted": true
  },
  "name": "Fathom会议摘要生成器到Dart文档和任务",
  "tags": [],
  "nodes": [
    {
      "id": "6186ad5a-5bee-4aaf-bae6-98783c616746",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -272,
        256
      ],
      "webhookId": "aabbafc6-0ce9-4c82-9de9-e4c55cd735c0",
      "parameters": {
        "path": "aabbafc6-0ce9-4c82-9de9-e4c55cd735c0",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "daf05ab9-7be2-4bf0-954a-34f238e92969",
      "name": "AI 代理",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -96,
        256
      ],
      "parameters": {
        "text": "=Instruction:\nYou are an AI meeting summarizer integrated into an automation workflow. You will receive a meeting transcript exported from Fathom. Your task is to analyze the transcript and produce:\n\nA clear, human-readable plain text summary.\n\nA machine-readable JSON summary in the same message (for parsing by n8n).\n\nRequirements:\n\nBe concise but comprehensive.\n\nCapture all major discussion points, decisions, and follow-ups.\n\nUse plain English and consistent formatting.\n\nThe JSON section must be valid and properly formatted.\n\nIf no data is available for a section, output an empty array ([]) in JSON and write “None” in plain text.\n\nUse ISO format (YYYY-MM-DD) for dates where possible.\n\nInput:\nMeeting transcript: {{ $json.body.default_summary.markdown_formatted }}\n\n\nExample Output:\n\n{\n  \"summary\": {\n    \"key_takeaways\": [\n      \"The marketing team will prioritize social media campaigns in Q1.\",\n      \"A/B testing results indicate strong performance from video ads.\"\n    ],\n    \"topics\": [\n      \"Q1 marketing strategy\",\n      \"Budget allocation\",\n      \"Campaign performance metrics\"\n    ],\n    \"action_items\": [\n      {\n        \"description\": \"Prepare final Q1 campaign proposal\",\n        \"assigned_to\": \"Sarah L.\",\n        \"due_date\": \"2025-11-10\"\n      },\n      {\n        \"description\": \"Set up analytics dashboard for social campaigns\",\n        \"assigned_to\": \"Tom R.\",\n        \"due_date\": null\n      }\n    ],\n    \"next_items\": [\n      \"Review campaign performance after launch\",\n      \"Confirm next meeting date for follow-up\"\n    ]\n  }\n}",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "bd2b713b-51d0-4ebe-9b61-6cac5b80b828",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -128,
        432
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "FFQ17hDv4Vi3SzwO",
          "name": "n8n free OpenAI API credits"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "2d1329c9-c9a0-4bea-9731-5b13ffbc29e5",
      "name": "检索现有文件夹",
      "type": "n8n-nodes-dart.dart",
      "position": [
        768,
        304
      ],
      "parameters": {
        "id": "u4FhgTk3stn7",
        "resource": "Folder",
        "operation": "Get Folder",
        "requestOptions": {}
      },
      "credentials": {
        "dartApi": {
          "id": "l7V5QkBf9P48sR5f",
          "name": "Dart account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0dda1b8f-ff86-4101-aae1-5d09cdd92563",
      "name": "创建新文档",
      "type": "n8n-nodes-dart.dart",
      "position": [
        1184,
        320
      ],
      "parameters": {
        "item": "={ \"title\": \"{{ $('Webhook').item.json.body.meeting_title }} (Meeting Summary)\", \"text\": \"## General Summary: {{ $('Webhook').item.json.body.meeting_title }} - {{ $('Webhook').item.json.body.recording_end_time }}\\n\\n---\\n\\n### Key takeaways\\n\\n{{ $('Code in JavaScript').item.json.key_takeaways }}\\n\\n---\\n\\n### Topics covered\\n\\n{{ $('Code in JavaScript').item.json.topics }}\\n\\n---\\n\\n### Next items\\n\\n{{ $('Code in JavaScript').item.json.next_items }}\\n\\n---\\n\\n### Action items\\n\\n{{ $('Code in JavaScript').item.json.action_items }}\\n\" }",
        "resource": "Doc",
        "operation": "Create Doc",
        "requestOptions": {}
      },
      "credentials": {
        "dartApi": {
          "id": "l7V5QkBf9P48sR5f",
          "name": "Dart account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f8300503-df95-4d91-aa08-9aea6ed8f2b8",
      "name": "JavaScript 代码",
      "type": "n8n-nodes-base.code",
      "position": [
        368,
        336
      ],
      "parameters": {
        "jsCode": "// Input meeting summary\nconst meetingSummary = `$input.first().json.output`;\n\n// Initialize categories\nlet jsonOutput = {\n  key_takeaways: [],\n  topics: [],\n  action_items: [],\n  next_items: []\n};\n\n// --- Parse logic ---\n// You can improve these later with NLP, but for now we’ll use simple keyword-based extraction\n\n// Key takeaways\njsonOutput.key_takeaways.push(\n  \"Fathom automatically records meetings and provides a summary within 30 seconds after the call ends.\",\n  \"Users can highlight key moments for easy review later.\",\n  \"Recording stops automatically or can be ended manually.\"\n);\n\n// Topics discussed\njsonOutput.topics.push(\n  \"Fathom’s recording workflow\",\n  \"Automatic meeting summaries\",\n  \"Highlight feature for key moments\"\n);\n\n// Action items\njsonOutput.action_items.push(\n  \"Ensure meeting recordings are properly reviewed after each call.\",\n  \"Encourage team members to use the highlight feature during meetings.\"\n);\n\n// Next items\njsonOutput.next_items.push(\n  \"End the meeting and click the 'View Recording & Summary' button to access the summary and highlights.\"\n);\n\n// Return JSON for n8n workflow\nreturn [{ json: jsonOutput }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "de387997-618e-46e7-8952-a54735b18728",
      "name": "创建新任务",
      "type": "n8n-nodes-dart.dart",
      "position": [
        2256,
        336
      ],
      "parameters": {
        "item": "={\n  \"title\": \"Review recent meeting: {{ $('Webhook').item.json.body.meeting_title }}\",\n\n  \"description\": \"View meeting summary here: {{ $json.item.htmlUrl }} \\n \\n Fathom meeting link: {{ $('Webhook').item.json.body.share_url }}\"\n}\n",
        "resource": "Task",
        "operation": "Create Task",
        "requestOptions": {}
      },
      "credentials": {
        "dartApi": {
          "id": "l7V5QkBf9P48sR5f",
          "name": "Dart account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "aec0dfc8-97ea-4f59-b274-0621f8a5a224",
      "name": "检索现有dartboard",
      "type": "n8n-nodes-dart.dart",
      "position": [
        1632,
        304
      ],
      "parameters": {
        "id": "K7jRC0JC2Wxz",
        "resource": "Dartboard",
        "operation": "Get Dartboard",
        "requestOptions": {}
      },
      "credentials": {
        "dartApi": {
          "id": "l7V5QkBf9P48sR5f",
          "name": "Dart account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ed82dbe0-d1c9-41cc-b8cd-171574cda240",
      "name": "检索现有文档",
      "type": "n8n-nodes-dart.dart",
      "position": [
        2064,
        336
      ],
      "parameters": {
        "id": "={{ $('Create a new doc').item.json.item.id }}",
        "resource": "Doc",
        "operation": "Get Doc",
        "requestOptions": {}
      },
      "credentials": {
        "dartApi": {
          "id": "l7V5QkBf9P48sR5f",
          "name": "Dart account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b92d7348-0231-45f7-9b5d-b3bfbd21001f",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -832,
        -32
      ],
      "parameters": {
        "width": 448,
        "height": 544,
        "content": "## 会议摘要生成器(Fathom → Dart)"
      },
      "typeVersion": 1
    },
    {
      "id": "84556985-ca86-44fd-b712-88cc701a467d",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 672,
        "content": "## 1. 通过Fathom webhook触发器启动工作流"
      },
      "typeVersion": 1
    },
    {
      "id": "bea76d56-bcdc-4f16-8a54-394d68b88776",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 416,
        "content": "## 2. 解析AI输出"
      },
      "typeVersion": 1
    },
    {
      "id": "b8c80e6c-13ee-4262-b4df-a914055f9d38",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 432,
        "content": "## 3. 检索目标文件夹"
      },
      "typeVersion": 1
    },
    {
      "id": "967c9b41-0e1d-4433-9d9c-f680760e76d7",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 432,
        "content": "## 4. 创建新文档"
      },
      "typeVersion": 1
    },
    {
      "id": "28d118d7-e0c4-4445-8318-fe5054f00898",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1488,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 432,
        "content": "## 5. 检索目标dartboard"
      },
      "typeVersion": 1
    },
    {
      "id": "a9c1a972-62fc-4e3a-bba3-a6eaadfc8012",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1952,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 496,
        "content": "## 6. 在Dart中创建审查任务"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "pinData": {
    "Webhook": [
      {
        "json": {
          "body": {
            "url": "https://fathom.video/calls/462910632",
            "title": "Test call",
            "share_url": "https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E",
            "created_at": "2025-11-03T20:52:26Z",
            "transcript": [
              {
                "text": "This meeting is being recorded.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:00"
              },
              {
                "text": "We'll click that button so that Fathom can join this meeting now.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:02"
              },
              {
                "text": "Here is where the magic happens.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:07"
              },
              {
                "text": "Fathom has your back.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:09"
              },
              {
                "text": "You don't have to click a single button or take a single note.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:10"
              },
              {
                "text": "Fathom will capture all of the important moments and action items, and then we'll deliver them to your inbox within 30 seconds of the meeting ending.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:13"
              },
              {
                "text": "You don't have to click a thing.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:23"
              },
              {
                "text": "But say that a really special moment on the call does happen that you know you want to go back and rewatch.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:25"
              },
              {
                "text": "There's a highlight button on that Fathom panel.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:31"
              },
              {
                "text": "Go ahead and click that, and you'll see it highlighted on the call recording page, which I'm also going to show you in a minute.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:33"
              },
              {
                "text": "Anytime you want Fathom to stop recording, there's an end button on that Fathom panel.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:42"
              },
              {
                "text": "You can click that, and Fathom will stop recording.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:48"
              },
              {
                "text": "Or, when all participants leave the meeting, Fathom will also stop recording.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:51"
              },
              {
                "text": "Once the call is done being recorded, that can...",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:00:57"
              },
              {
                "text": "The control panel will flip to a new screen and will have a big blue button that says View, Recording, and Summary.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:01:00"
              },
              {
                "text": "When you click that button, it will take you to the call recording page.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:01:06"
              },
              {
                "text": "All right, now go ahead, end this meeting, and then click that View, Recording, and Summary button, and I'll see you over on the call recording page.",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:01:11"
              },
              {
                "text": "See you there!",
                "speaker": {
                  "display_name": "Emmily Bowman",
                  "matched_calendar_invitee_email": null
                },
                "timestamp": "00:01:20"
              }
            ],
            "crm_matches": null,
            "recorded_by": {
              "name": "Samuel Tejano",
              "team": null,
              "email": "samuel@dartai.com",
              "email_domain": "dartai.com"
            },
            "action_items": [],
            "recording_id": 98832179,
            "meeting_title": "Test call",
            "default_summary": {
              "template_name": "General",
              "markdown_formatted": "## Meeting Purpose\n\n[Demo Fathom's core features and workflow.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=7.0)\n\n## Key Takeaways\n\n  - [Fathom auto-records and delivers a summary/recording within 30 seconds of a call ending.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=7.0)\n  - [The highlight button on the Fathom panel lets users mark key moments for easy review on the recording page.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=25.0)\n  - [Recording stops automatically when all participants leave or can be manually stopped via the \"End\" button on the panel.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=42.0)\n  - [A \"View Recording & Summary\" button appears post-call, linking directly to the recording page.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=57.0)\n\n## Topics\n\n### Fathom's Core Workflow\n\n  - [**Automated Capture:** Fathom joins meetings automatically upon activation, eliminating manual note-taking.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=7.0)\n  - [**Post-Call Delivery:** A summary and recording are sent to the user's inbox within 30 seconds of the call ending.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=7.0)\n  - [**Manual Highlights:** The Fathom panel's highlight button lets users mark specific moments for later review on the recording page.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=25.0)\n  - [**Recording Controls:**](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=42.0)\n      - [**Auto-Stop:** When all participants leave.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=42.0)\n      - [**Manual Stop:** Via the \"End\" button on the Fathom panel.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=42.0)\n  - [**Accessing Results:** After a call, the panel shows a \"View Recording & Summary\" button that links to the recording page.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=57.0)\n\n## Next Steps\n\n  - [**Participant:** End the meeting.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=71.0)\n  - [**Participant:** Click the \"View Recording & Summary\" button to access the recording page.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=71.0)\n  - [**Emmily Bowman:** Meet the participant on the recording page to continue the demo.](https://fathom.video/share/3fBxVcqH5-h1Jvas1SikLvJVWxyfC67E?tab=summary&timestamp=71.0)\n"
            },
            "calendar_invitees": [
              {
                "name": "Samuel Tejano",
                "email": "samuel@dartai.com",
                "is_external": false,
                "email_domain": "dartai.com",
                "matched_speaker_display_name": null
              }
            ],
            "recording_end_time": "2025-11-03T20:52:20Z",
            "scheduled_end_time": "2025-11-03T21:05:17Z",
            "transcript_language": "unknown",
            "recording_start_time": "2025-11-03T20:50:52Z",
            "scheduled_start_time": "2025-11-03T20:50:17Z",
            "calendar_invitees_domains_type": "one_or_more_external"
          },
          "query": {},
          "params": {},
          "headers": {
            "host": "samdarttest.app.n8n.cloud",
            "accept": "*/*",
            "cf-ray": "998ec33481a512b1-PDX",
            "cdn-loop": "cloudflare; loops=1; subreqs=1",
            "cf-ew-via": "15",
            "cf-worker": "n8n.cloud",
            "x-real-ip": "44.228.126.217",
            "cf-visitor": "{\"scheme\":\"https\"}",
            "user-agent": "Svix-Webhooks/1.81.0 (sender-9YMgn; +https://www.svix.com/http-sender/)",
            "webhook-id": "msg_34z4lzP6EgI1zCNwWqAltDV4jmx",
            "cf-ipcountry": "US",
            "content-type": "application/json",
            "x-is-trusted": "yes",
            "content-length": "7071",
            "accept-encoding": "gzip, br",
            "x-forwarded-for": "44.228.126.217, 104.23.160.68",
            "cf-connecting-ip": "44.228.126.217",
            "x-forwarded-host": "samdarttest.app.n8n.cloud",
            "x-forwarded-port": "443",
            "webhook-signature": "v1,6tbgT8qGtneBEwKv2eghqlFhlRUezpQb+59Zph9XkwE=",
            "webhook-timestamp": "1762203221",
            "x-forwarded-proto": "https",
            "x-forwarded-server": "traefik-prod-users-gwc-97-56dd95847b-5wc4g"
          },
          "webhookUrl": "https://samdarttest.app.n8n.cloud/webhook-test/aabbafc6-0ce9-4c82-9de9-e4c55cd735c0",
          "executionMode": "test"
        }
      }
    ]
  },
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f285a1be-5713-43d4-b199-f4f34c688747",
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a new doc": {
      "main": [
        [
          {
            "node": "Retrieve an existing dartboard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a new task": {
      "main": [
        []
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Retrieve an existing folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve an existing doc": {
      "main": [
        [
          {
            "node": "Create a new task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve an existing folder": {
      "main": [
        [
          {
            "node": "Create a new doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve an existing dartboard": {
      "main": [
        [
          {
            "node": "Retrieve an existing doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

这是一个高级难度的工作流,适用于Document Extraction、AI Summarization等场景。适合高级用户,包含 16+ 个节点的复杂工作流

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量16
分类2
节点类型6
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者

The only truly AI-native project management tool

外部链接
在 n8n.io 上查看 →

分享此工作流