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