← ClearskiesCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Clearskies
Snapshot Sep 30, 2026 · 22:57 UTC · version 1.0.2
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"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"
}SHA-256: b70f0bb205b29f6f87a0ad74273f9ee02d9e84139d46bc5d22042f963ef808e9