{"id":10591,"plugin_id":"plugin_asdk_app_6a5a6880b6dc8191897d607c3256652d","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T22:57:33.246Z","digest":"b70f0bb205b29f6f87a0ad74273f9ee02d9e84139d46bc5d22042f963ef808e9","against":null,"payload":{"name":"clearskies-workflow-builder","description":"Author, edit, validate, and safely publish workflows and agents on the clearskies workflow-builder MCP (tools workflow_capabilities_get, workflows_create/update/publish, workflow_validate, workflow_variables_get, workflow_test_run_start, workflow_runs_get/list, agents_*, object_*). Use whenever the user wants to build, change, debug, or publish a clearskies workflow or agent — e.g. \"make a workflow that…\", \"update the workflow that…\", \"why did my workflow fail\", \"publish this workflow\" — or describes a scheduled/call/Salesforce-triggered automation that finds CRM records, runs an agent, posts Slack, sends email, or writes Salesforce, even without saying \"workflow\" (e.g. \"every morning DM me my open deals\"). Key rule: workflow_validate checks structure and references but not runtime behavior, so ALWAYS dry-run with workflow_test_run_start before publishing.","included_files":[{"relative_path":"references/lessons.md","size_in_bytes":17733},{"relative_path":"scripts/inspect_run.sh","size_in_bytes":2237}],"skill_md_contents":"---\nname: clearskies-workflow-builder\ndescription: >-\n  Author, edit, validate, and safely publish workflows and agents on the clearskies workflow-builder MCP (tools\n  workflow_capabilities_get, workflows_create/update/publish, workflow_validate,\n  workflow_variables_get, workflow_test_run_start, workflow_runs_get/list, agents_*,\n  object_*). Use whenever the user wants to build, change, debug, or publish a\n  clearskies workflow or agent — e.g. \"make a workflow that…\", \"update the\n  workflow that…\", \"why did my workflow fail\", \"publish this workflow\" — or describes a\n  scheduled/call/Salesforce-triggered automation that finds CRM records, runs an agent,\n  posts Slack, sends email, or writes Salesforce, even without saying \"workflow\" (e.g.\n  \"every morning DM me my open deals\"). Key rule: workflow_validate checks structure and\n  references but not runtime behavior, so ALWAYS dry-run with workflow_test_run_start\n  before publishing.\n---\n\n# clearskies workflow builder\n\nBuilding a clearskies workflow looks easy and is full of quiet traps. The tools\nhappily accept configurations that validate clean and then silently misbehave at\nruntime — writing to the wrong record, resolving a variable to empty string, or\nfinding nothing. This skill encodes the order of operations and the specific traps\nso a workflow you ship actually does what the user asked.\n\n## Load connected-data context\n\nWhen object or field discovery is needed, call `schema_search` first with the workflow's\nbusiness concept. Use the ranked results only to select candidates; keep pagination\ncursors with the same query and filters, disclose warnings, and call\n`object_get_fields_schema` for each selected object before authoring filters or writes.\nUse `object_definitions_list` only when a complete configured object list is required. For\naudits, enumerate every field on each selected and related object with\n`object_get_fields_schema` rather than treating search results as exhaustive.\n\n## The golden rule\n\n**`workflow_validate` confirms the graph is well-formed and that its references\nresolve** — it rejects dangling `{{...}}` references, template paths a trigger doesn't\nexpose, unknown filter `fieldId`s, cycles, and a `findRecords` with no selection method\n(unknown Salesforce objects come back as a warning). **What it cannot do is prove the\nworkflow behaves correctly at runtime:** whether a find actually returns records, that\nan `updateSalesforce` targets more than the first record, that a\n`{{find.records.some_field}}` projection resolves, or that a structured filter's\n`fieldId` resolves on the workflow path. A `{\"valid\": true}` means \"well-formed and\nreferences exist,\" not \"this works.\" The only trustworthy behavioral check is a **dry\nrun** (`workflow_test_run_start` → `workflow_runs_get`), where you read the actual\nper-step inputs and outputs. Never publish on validation alone.\n\n## Order of operations\n\nFollow these in order. Don't skip to publishing.\n\n1. **Read the available workflow capabilities.** Call `workflow_capabilities_get` first — node\n   types, trigger types, and other options can change over time. It is the source of truth\n   for shape; `references/lessons.md` is the\n   source of truth for the traps it omits.\n2. **Learn from a working example.** Call `workflows_list` with `includeConfig: true`\n   and read a published workflow similar to the target. This is how you discover real\n   filter field ids and proven node shapes — see the \"field IDs\" trap below.\n3. **Design the graph**, applying the traps in `references/lessons.md`. Add a\n   `runAgent` node only if the task needs real AI reasoning; skip it for pure data\n   forwarding.\n4. **Create or update as a draft** (`workflows_create` / `workflows_update`). Both\n   write a draft; nothing runs until you publish. On a published workflow,\n   `workflows_update` writes a pending draft and leaves the live version untouched\n   until you publish. For incremental edits to an existing draft, `workflow_node_patch`\n   applies an ordered batch of add/update/replace/delete node ops (wiring the edges for\n   you) without resending the whole config; it's mutate-only, so still validate and\n   publish afterward. Use `workflows_update` with the full configuration when you need\n   parallel branches.\n5. **Validate** (`workflow_validate`) and fix the per-node errors. Necessary but not\n   sufficient.\n6. **VALIDATE BY DRY RUN — the mandatory step.** See the next section. Do not skip\n   this even if validation passed and the graph \"looks obviously correct.\"\n7. **Publish** (`workflows_publish`) only after the dry run confirms behavior. If the\n   workflow references an agent, publish the agent first (`agents_publish`) — a\n   workflow can't publish while pointing at an unpublished agent.\n\n## The mandatory validation step (valid + invalid dry runs)\n\nValidation that only runs the happy path tells you little. Prove the workflow both\n*does the right thing* on good input and *visibly fails* on bad input — that\nconfirms your assertions actually discriminate, and it surfaces the silent-empty\nand wrong-record traps that `workflow_validate` cannot.\n\n**Safety:** `workflow_test_run_start` is **always** a dry run — it never writes to\nSalesforce (write steps report `dryRun: true` and show the exact recordId/field/value\nthey *would* have written), and no Slack message or email is actually sent. Testing is\ntherefore safe by construction; you do not need any special setting to protect a test run.\n\n`requireHumanReview` is a **production-only** control, unrelated to testing: on a\n*live/published* workflow it holds the Salesforce write for a human to approve instead\nof auto-applying it. Decide it by production intent — set it when you want a human in\nthe loop on live writes; leave it off when you want the published workflow to\nauto-write. Neither of these facts — that test runs never write, and that this setting\nis production-only — is stated in the tool docs, so make it explicit to whoever\ninherits the workflow.\n\n**Procedure:**\n\n1. **Pick a real trigger input (non-scheduled triggers).** Scheduled triggers need no\n   input. Call triggers (`callStarted`/`callEnded`) need a `meetingId`; record triggers\n   (`salesforceRecordCreated`) need a `recordId`. **Never fabricate these ids** — query\n   the MCP for real data: `workflow_trigger_records_list(workflowId)` returns valid\n   candidate inputs for that workflow's trigger (meetings or records), with `search`.\n   To choose deliberately, confirm which candidates satisfy vs. violate your\n   workflow's filter using the object query tools (`crm_records_list`, `accounts_list`,\n   `deals_list`, `events_list`/`events_search`, `object_get_fields_schema`). Testing on\n   real records is what makes the dry run meaningful — a made-up id tests nothing.\n2. **Run the valid case.** `workflow_test_run_start` with the chosen real input (a\n   `recordId` or `meetingId` that *does* meet the workflow's condition; none for\n   scheduled). Poll `workflow_runs_get <testRunId>`.\n   Run traces containing emails/accounts are large (50–80 KB) and will overflow —\n   **extract with `jq`, never read the whole blob** (`scripts/inspect_run.sh` does\n   this for you).\n   - Assert: run `status: completed`; each step `completed`; the resolved\n     `inputs`/`outputs` contain **real values** — real `sfRecordId`s, non-empty\n     fields, expected `count > 0`. An empty `count`, an empty `recordId`, or a\n     value like `\"FIELD=[]\"` means a reference silently resolved to nothing.\n3. **Run at least one invalid/negative case** — confirm the workflow does the right\n   thing when it *shouldn't* fire or when a reference is wrong. Choose the probe by\n   trigger type:\n   - **Non-scheduled triggers → feed a real record/meeting that legitimately lacks the\n     condition.** Don't fabricate an id and don't mangle the config — pull a genuine\n     counter-example via `workflow_trigger_records_list` (+ the object query tools to\n     find one that violates the filter), pass its real `recordId`/`meetingId`, and\n     confirm the workflow stops at the filter / produces no action. This proves the\n     gating works on real data the way production will see it.\n   - **Reference integrity (any trigger)** — a **field projection off a find result**\n     (`{{find-1.records.no_such_field}}`) resolves to an empty string silently:\n     validation can't know a record's fields, so the run still reports success. Confirm\n     it resolves to a real value, which also proves your happy-path values weren't\n     accidental.\n   - **Empty find** → confirm `count: 0` and that a downstream single-record\n     `updateSalesforce` then errors `\"resolved Salesforce record ID is empty\"` (proves\n     the find is actually filtering).\n   - **Multi-record find feeding a bare update** (no loop) → confirm only the **first**\n     record is targeted (the single-record trap).\n4. **Compare against expectation and report.** State plainly what each run proved.\n   Only after the valid case behaves and the invalid case fails-as-expected should\n   you publish.\n\nIf a dry run contradicts your mental model, trust the trace and fix the graph —\nthat is the entire point of this step.\n\n## Call triggers (`callStarted` / `callEnded`): screen out non-meetings\n\nCalendar sync is a coarse filter. It excludes only *native* non-meeting event types\n(out-of-office, focus time, working-location, birthday) and cancelled events, and it\ndrops events starting more than a year out. **Everything else becomes a meeting the\ntrigger can fire on** — including ordinary personal **holds, prep/blocks,\nplaceholders, \"busy\" entries, and declined or tentative invites.** None of those are\nscreened by title or availability at the trigger layer.\n\nThe consequence: a `callStarted` workflow with no real filter will fire on someone's\n\"Hold — do not book\" or \"Prep\" block and spam Slack/email/Salesforce. (`callEnded` is\nnaturally safer — it generally won't fire for a pure hold because there's no linked,\ncompleted call/transcript — but filter it too; don't rely on that.)\n\n**Do not filter on \"has a video-conferencing link.\"** It's tempting to add \"has a real\nZoom/Meet/Teams link\" as a quality signal, and it seems like the obvious way to reject\nholds/blocks — but the `meeting.*` data a workflow can see has **no such field**.\n`object_get_fields_schema(meeting)` and `workflow_variables_get` both cap out at:\n`accounts`, `attendees`, `calendar_event_canonical_instance_id`,\n`calendar_event_source_id`, `calendar_event_provider` (which calendar system, e.g.\nGoogle/Outlook — not a join link), `duration_seconds`, `end_at`, `host`, `id`,\n`participants`, `start_at`, `title`. The actual Zoom/Meet URL lives only in the raw\ncalendar event's free-text `description` (visible via `events_get_contents`), a field\nworkflows never receive. An `aiFilterPrompt` asked to check for a link is therefore\nguessing from the title/attendees with no real signal, and it **will false-negative on\ngenuine, already-recorded customer calls**. If you need to reject non-calls, use\nsignals that are actually exposed: `duration_seconds` (a 0-duration or never-started\nhold has none/very little), attendee count and `person_type`, response status, and\ntitle text — not a conferencing-link check.\n\n**So every call-triggered workflow needs a filter node right after the trigger that is\na real quality gate, not a rubber stamp.** (Call-triggered workflows are required to\nhave a filter immediately after the trigger anyway — make that requirement earn its\nkeep.) Decide what \"a real meeting worth acting on\" means for the use case, then\nencode it. A good general-purpose `aiFilterPrompt` default:\n\n```\nOnly proceed if this is a genuine meeting worth acting on:\n- it has at least one attendee outside our own company (an external/customer domain),\n- the current user has NOT declined it,\n- it has a nonzero duration (duration_seconds > 0) consistent with a call that\n  actually happened, and\n- it is NOT a personal hold, focus/time block, prep or reminder, or placeholder\n  (e.g. titles like \"Hold\", \"Prep\", \"Block\", \"Busy\", \"OOO\", \"Placeholder\", \"Focus\",\n  \"Reminder\", or an empty/1-attendee event).\nOtherwise stop.\n```\n\nTighten or loosen per use case — an internal-standup digest wants the opposite of the\n\"external attendee\" clause. Prefer `aiFilterPrompt` here because hold/prep/placeholder\ndetection is fuzzy and title-dependent; use a structured `filter` when you have a\ncrisp signal (e.g. attendee count, a known internal domain, response status) — but\nnever a conferencing-link check, structured or AI, since the data isn't there.\n\n**Validate it with a real junk meeting.** This is the ideal negative case for the\nmandatory validation step: use `workflow_trigger_records_list` to find a real \"Hold\"\n/ \"Prep\" / calendar-block event, dry-run it, and confirm the workflow stops\nat the filter. Then dry-run a real customer meeting and confirm it proceeds.\n\n## The traps (read before building)\n\nFull detail, with evidence, is in `references/lessons.md`. The ones that bite most:\n\n- **Prefer `aiFilterPrompt` / `aiFindPrompt` over structured filters.** A structured\n  filter's `fieldId` is finicky — it must resolve on the workflow path, and one that\n  looks right can still fail at runtime with `\"filter field not found\"`. The AI-prompt\n  forms take natural language (and interpolate `{{...}}`), sidestep field ids, and are\n  the reliable default. If you must use a structured filter, copy the exact `fieldId`\n  from a working workflow's config (`workflows_list includeConfig:true`) and confirm it\n  in a dry run.\n- **Traverse relationships by chaining finds, not by dotting the variable tree.**\n  To act on records related to a prior step, add a second `findRecords` filtered\n  `isIn {{find-1.records}}` (or `{{find-1.records.<field>}}` / `<relation>`), then act\n  on `{{find-2.records.sfRecordId}}`. Relations resolve at runtime **even when\n  `workflow_variables_get` omits them** — don't conclude \"unreachable\" from the\n  variable tree; a dry run is the arbiter.\n- **`updateSalesforce` is single-record.** `{{find.records.sfRecordId}}` targets only\n  the **first** record and still reports `completed`. To update N records, wrap the\n  update in a `loop` over `{{find.records}}` and use `{{loop-1.sfRecordId}}`.\n- **No sub-day relative dates.** Date filters support day-granular relatives\n  (`yesterday`, `{\"relativeDate\":\"numberOfDaysAgo\",\"value\":N}`) or RFC3339 — there is\n  no \"last 4 hours\". For short windows use an `aiFindPrompt` (\"emails in the last 4\n  hours\"). `now-4h`-style strings are accepted silently and never work.\n- **A field projection off a find result can resolve to empty silently.**\n  `{{find-1.records.no_such_field}}` — or any reference that resolves to an empty\n  collection — becomes `\"\"`/`[]` at runtime with no error; for a Salesforce write that\n  blanks the target field. Always confirm resolved values in the dry-run trace.\n- **Use `workflow_variables_get` to confirm a `{{...}}` path exists.** It takes an\n  inline configuration you're drafting *or* a saved `workflowId`, and returns a compact\n  tree — direct fields as dot-path `systemFields`, related objects as drillable\n  `references` (`subFields` / `drillable`) rather than a fully-expanded graph. Reference\n  only paths it confirms, but remember relations resolve at runtime even before you\n  drill them (see the relationship-traversal trap in `references/lessons.md`).\n- **A `runAgent` summarizer may append meta-commentary** (\"want me to draft a\n  follow-up?\") to its output, which then flows verbatim into a Slack message or\n  email. If the agent's job is to produce a message body, tell its template\n  explicitly to output only that text with no questions or offers of further work,\n  and confirm the fix in a dry run.\n\n## Reference & helper files\n\n- `references/lessons.md` — the complete, evidence-backed trap list, the draft/publish\n  model, trigger/node field details, known organization field ids, and worked\n  valid/invalid dry-run examples. Read it before building anything non-trivial.\n- `scripts/inspect_run.sh` — `bash scripts/inspect_run.sh <run-json-file>` prints the\n  per-step status/errors and resolved inputs/outputs from an overflowed\n  `workflow_runs_get` result without dumping the whole blob into context.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}