Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- **SDK methods named after MCP tools**: `PipefyClient.get_ai_automation`, `get_ai_automations`, `delete_ai_automation`, `remove_member_from_pipe`, and `fill_card_phase_fields` expose those operations as client methods, with the MCP tool names and parameters. The corresponding MCP tools and CLI commands now call these methods (MCP `fill_card_phase_fields` still elicits when a form can be shown). The AI-list filter, member-removal verification, and editable-field filter move into the SDK. Skills retarget `get_labels` to `get_pipe` and `get_pipe_report` to `get_pipe_reports`; those two names stay MCP aliases (projection remains in MCP and CLI). (#696)

- **SDK pre-write validation**: `PipefyClient.validate_ai_agent_behaviors` and `PipefyClient.validate_ai_automation_prompt` expose the two read-only validators as client methods, with the MCP tool names and parameters, so an agent that builds its tools from `PipefyClient` can validate before it writes. The MCP tools and CLI commands now call these methods. The `pipefy_sdk.ai_preflight` module functions stay. (#694)

### Changed

- **SDK AI agent create**: `PipefyClient.create_ai_agent` now writes the agent's instruction and behaviors. It creates the agent and chains `update_ai_agent`, as the MCP tool and CLI command did on their own; before, it dropped the required `instruction` and `behaviors` and returned an empty, disabled agent. When the update fails, it raises the new `AiAgentConfigureError`, which carries the created `agent_uuid`. `CreateAiAgentInput` and `UpdateAiAgentInput` now expand `template_params` / `instruction_template` and normalize instruction token aliases while they validate, so SDK callers get the same prep as the MCP tools. As a result, a raw behavior dict with a literal `{{name}}` and no `template_params` now fails `CreateAiAgentInput` / `UpdateAiAgentInput` validation, as it already failed in the MCP tools. Callers that expanded behaviors themselves can drop that step: a second expansion fails when a substituted value contains `{{name}}`. The MCP tools and CLI commands call these methods. A CLI `agent create` whose update fails now prints the created agent's UUID. The unused `pipefy_mcp.tools.behavior_placeholder_interpolation` re-export is removed; import the helpers from `pipefy_sdk.behavior_placeholders`. (#695)

- **MCP `fill_card_phase_fields`**: with `skip_elicitation=true` or no back channel, no longer writes when the phase has no editable fields; dropped keys return in `skipped_field_ids`. CLI `pipefy card fill --fields {}` on a phase with editable fields now returns the collected-nothing envelope instead of short-circuiting with "No fields to update."

- **CLI `pipefy member remove`**: verifies membership after the mutation and returns `warning` (`null` when every member is gone).

### Fixed

- **Automation listings**: SDK, MCP, and CLI now return trigger IDs, event parameters, conditions, `actionEnabled`, and `disabledReason` for organization and pipe listings, avoiding a detail call per rule to audit its filters and whether the action is enabled. Listings are paged: the API caps a page at 50 rules, so `get_automations` / `pipefy automation list` accept `first` / `after` and report `totalCount` and `hasNextPage` instead of silently returning the first 50. `get_ai_automations` / `pipefy ai-automation list` expose the same page block for the mixed connection they filter. Human `pipefy automation list` prints a table of each row's scalar columns plus the page counts, leaving the nested `event_params` and `condition` to `--json`. Phase-delete preview follows every page of rules, reads their details under a concurrency bound instead of one simultaneous call per rule, and says when the dependents list is a lower bound because a page or a detail read failed. An empty pipe no longer fails `get_automation_logs_by_repo`. The last page of an audit names itself so `11 of 61` is not read as a shortfall. (#612)
Expand Down
6 changes: 3 additions & 3 deletions docs/mcp/tools/pipes-and-cards.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ Pipefy’s GraphQL API uses **string** IDs for pipes, phases, cards, and most ot
When elicitation is unavailable, `create_card` and `fill_card_phase_fields` still work but behave differently. That covers agents, CLIs, and SDK consumers, and also **the hosted server**, which serves `json_response=True` and so has no server-to-client back channel at any protocol revision:

1. The tool fetches the start-form or phase field definitions internally.
2. Provided `fields` are **filtered to editable field IDs only** — keys that do not match an editable field are silently discarded (no error).
3. The filtered dict is sent directly to the Pipefy API.
2. For `create_card`, provided `fields` are **filtered to editable field IDs only** — keys that do not match an editable field are silently discarded (no error). The filtered dict is sent directly to the Pipefy API.
3. For `fill_card_phase_fields`, keys the phase does not expose as editable are returned in `skipped_field_ids`. When nothing survives the filter, nothing is written.

Because non-editable keys are dropped without warning, agents should discover fields first and pass all required values explicitly:
Agents should discover fields first and pass all required values explicitly:

```
get_start_form_fields(pipe_id) → learn field IDs, types, required flag
Expand Down
4 changes: 2 additions & 2 deletions docs/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ MCP destructive tools use a two-step `confirmation_token` (see [Destructive oper
| `export_organization_report` | `pipefy report-org export` | shipped | Exports + organization reports. |
| `export_pipe_audit_logs` | `pipefy audit export` | shipped | (`--pipe`); API queues export (JSON payload only). |
| `export_pipe_report` | `pipefy report-pipe export` | shipped | Exports + reports; `filter` preflight validates ReportCardsFilter shape (nested `operator` + `queries`). |
| `fill_card_phase_fields` | `pipefy card fill` | shipped | (`--phase`, `--fields` JSON, optional `--required-only`). Non-interactive; filters to editable phase field IDs before `update_card`. CLI is stricter than MCP when the phase has no editable fields (no-op vs unfiltered pass-through). Response may include `skipped_field_ids` for keys dropped by the filter. |
| `fill_card_phase_fields` | `pipefy card fill` | shipped | (`--phase`, `--fields` JSON, optional `--required-only`). Non-interactive. Both surfaces filter to editable phase field IDs and skip the write when nothing survives; `skipped_field_ids` lists dropped keys. |
| `find_cards` | `pipefy card find` | shipped | (`--pipe`, `--field`, `--value`). |
| `find_records` | `pipefy record find` | shipped | (`--filter` JSON with `field_id` + `field_value`). Unified MCP envelope: top-level `pagination` uses `has_more` / `end_cursor` / `page_size` (same as `get_table_records`). |
| `get_agents_usage` | `pipefy usage agents` | shipped | (`--organization`, `--from`, `--to`, optional `--filters` / `--search` / `--sort` JSON). |
Expand Down Expand Up @@ -166,7 +166,7 @@ MCP destructive tools use a two-step `confirmation_token` (see [Destructive oper
| `list_portals` | `pipefy portal list` | shipped | `--organization-uuid`; at most one main portal per org. |
| `move_card_to_phase` | `pipefy card move` | shipped | (`--phase`). On required-field failures MCP may return `success: false` naming the field (and an optional hide hint); CLI still returns the raw SDK / GraphQL error (known MCP-ahead behavior). |
| `publish_sub_portal` | `pipefy portal sub-portal publish` | shipped | internal_api `updateSubPortalElement` on a templated `forms` element; check `subPortals[].published` via `get_portal`. |
| `remove_member_from_pipe` | `pipefy member remove` | shipped | MCP two-step with `confirmation_token`; CLI `--yes` or interactive prompt. |
| `remove_member_from_pipe` | `pipefy member remove` | shipped | MCP two-step with `confirmation_token`; CLI `--yes` or interactive prompt. Both surfaces read the members back and return `warning` when a user is still present. |
| `reset_default_llm_provider` | `pipefy ai-provider default reset` | shipped | Organization-scoped; clears the org default (`--org-id`). |
| `search_pipes` | `pipefy pipe list` | shipped | (`--name`, `--max-per-org`). |
| `search_schema` | `pipefy introspect schema search` | shipped | (optional `--kind`). |
Expand Down
12 changes: 12 additions & 0 deletions docs/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ Two read-only `PipefyClient` methods dry-run a write before you make it. They ha

Both return a dict. `valid` is true only when `problems` is empty, and `warnings` holds non-blocking notices. The agent result adds a `message`; the prompt result adds a `field_map` of the referenced field IDs to their labels. A failed pipe read sets `success` to false instead of raising: the agent result then gives the reason in `problems`, and the prompt result carries only `success`, `valid`, and `error`.

## Methods named after the MCP tools

Five `PipefyClient` methods share names and parameters with MCP tools so an agent that builds its tools from the client can call the same operations. MCP and CLI call these methods; MCP `fill_card_phase_fields` still owns elicitation when a form can be shown and the caller did not skip it.

- **`get_ai_automation(automation_id)`** delegates to `get_automation`. Returns the rule record, or `None` when the id is missing.
- **`get_ai_automations(pipe_id, organization_id=None, *, first=None, after=None)`** calls `get_automations`, then keeps only `generate_with_ai` rows in `nodes` (`pipefy_sdk.ai_preflight.filter_ai_automation_summaries`). `totalCount` and `pageInfo` still describe the mixed page.
- **`delete_ai_automation(automation_id)`** delegates to `delete_automation`. Same result as that method.
- **`remove_member_from_pipe(pipe_id, user_ids)`** runs the mutation, then reads members back. Returns `{"data": <mutation result>, "warning": <str or None>}`. `warning` is `None` when every requested user is gone, when `pipe_id` is not numeric, or when the members read fails. Use the singular when the caller wants confirmation that the removal took effect. **`remove_members_from_pipe`** fetches the pipe before mutating and may fetch members when resolving numeric user ids; it skips post-mutation verification. The plural is not deprecated.
- **`fill_card_phase_fields(card_id, phase_id, fields, *, required_fields_only=False)`** reads `get_phase_fields` once, filters `fields` to editable ids, and does not write a field the phase does not expose. When nothing survives: `success`, `message`, `phase_id`, `phase_name`, `skipped_field_ids`. When a write runs: the `update_card` dict, plus `skipped_field_ids` only when a key was dropped.

Skills cite `get_pipe` (labels are on the pipe) and `get_pipe_reports` (the reports connection). There is no `PipefyClient.get_labels` or `PipefyClient.get_pipe_report`; MCP keeps those names as aliases, and projection stays in MCP and CLI.

## Configuration

OAuth and endpoint variables are documented in **[`../config.md`](../config.md)** and **[`../../.env.example`](../../.env.example)**. Integration tests use `@pytest.mark.integration` and the same `PIPEFY_*` keys from local **`.env`** (e.g. `PIPEFY_PORTAL_ORG_UUID` for portal live tests). Unit tests use fictional ids in **[`../../packages/sdk/tests/_shared/fixture_ids.py`](../../packages/sdk/tests/_shared/fixture_ids.py)** — not production org UUIDs.
Expand Down
12 changes: 5 additions & 7 deletions packages/cli/src/pipefy_cli/commands/ai_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
PipefyClient,
UpdateAiAutomationInput,
)
from pipefy_sdk.ai_preflight import filter_ai_automation_summaries
from pydantic import ValidationError

from pipefy_cli.commands._common import (
Expand Down Expand Up @@ -99,17 +98,16 @@ def ai_automation_list(
page_size = first if first is not None else AUTOMATIONS_LIST_MAX_PAGE_SIZE

async def factory(client: PipefyClient):
page = await client.get_automations(
page = await client.get_ai_automations(
pipe,
organization_id=organization,
pipe_id=pipe,
first=first,
after=cursor,
)
filtered = filter_ai_automation_summaries(page["nodes"])
info = page["pageInfo"]
return {
"success": True,
"data": filtered,
"data": page["nodes"],
"message": "AI automations listed.",
"pagination": {
"has_more": bool(info.get("hasNextPage")),
Expand All @@ -131,7 +129,7 @@ def ai_automation_get(
"""Load one automation row (``get_ai_automation`` / ``get_automation``)."""

async def factory(client: PipefyClient):
row = await client.get_automation(automation_id)
row = await client.get_ai_automation(automation_id)
if row is None:
return {
"success": False,
Expand Down Expand Up @@ -346,6 +344,6 @@ def ai_automation_delete(
)

async def factory(client: PipefyClient):
return await client.delete_automation(automation_id)
return await client.delete_ai_automation(automation_id)

run_cli_command(ctx, json_out, factory)
34 changes: 4 additions & 30 deletions packages/cli/src/pipefy_cli/commands/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
PipefyClient,
UpdateCommentInput,
copy_card_search,
filter_editable_field_definitions,
filter_fields_by_definitions,
skipped_field_ids,
)
from pydantic import ValidationError

Expand Down Expand Up @@ -408,39 +405,16 @@ def card_fill(
) -> None:
"""Fill phase fields on a card (non-interactive).

Filters ``--fields`` to editable phase field IDs before ``update_card``.
Stricter than MCP ``fill_card_phase_fields`` when the phase reports no
editable fields (CLI no-ops; MCP may pass values through unfiltered).
Filters ``--fields`` to editable phase field IDs. Dropped keys come back in
``skipped_field_ids``. When nothing survives, no write is issued.
"""

fields = parse_json_object(fields_json, "--fields") or {}

async def factory(client: PipefyClient):
if not fields:
return {"success": True, "message": "No fields to update."}

phase_fields_result = await client.get_phase_fields(phase_id, required_only)
expected_fields = filter_editable_field_definitions(
phase_fields_result.get("fields", [])
return await client.fill_card_phase_fields(
card_id, phase_id, fields, required_fields_only=required_only
)
field_data = filter_fields_by_definitions(fields, expected_fields)
dropped = skipped_field_ids(fields, field_data)
if not field_data:
result: dict[str, Any] = {
"success": True,
"message": "No fields to update.",
}
if dropped:
result["skipped_field_ids"] = dropped
return result
field_updates = [
{"field_id": field_id, "value": value}
for field_id, value in field_data.items()
]
api_response = await client.update_card(card_id, field_updates=field_updates)
if dropped:
return {**api_response, "skipped_field_ids": dropped}
return api_response

run_cli_command(ctx, json_out, factory)

Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/pipefy_cli/commands/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ def member_remove(
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
json_out: bool = typer.Option(False, "--json", "-j"),
) -> None:
"""Remove users from a pipe."""
"""Remove users from a pipe.

Reads members back after the mutation. Output includes ``data`` (mutation
result) and ``warning`` (null when every requested user is gone). A warning
string means a requested user is still present; org-level permissions can
override pipe-level removal.
"""

ids = _parse_user_ids(user_ids)

Expand All @@ -125,7 +131,7 @@ def member_remove(
)

async def factory(client: PipefyClient):
return await client.remove_members_from_pipe(pipe_id, ids)
return await client.remove_member_from_pipe(pipe_id, ids)

run_cli_command(ctx, json_out, factory)

Expand Down
10 changes: 7 additions & 3 deletions packages/cli/tests/fixtures/cli_help_golden.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1351,9 +1351,8 @@ Usage: pipefy card fill [OPTIONS] CARD_ID

Fill phase fields on a card (non-interactive).

Filters ``--fields`` to editable phase field IDs before ``update_card``.
Stricter than MCP ``fill_card_phase_fields`` when the phase reports no
editable fields (CLI no-ops; MCP may pass values through unfiltered).
Filters ``--fields`` to editable phase field IDs. Dropped keys come back in
``skipped_field_ids``. When nothing survives, no write is issued.

╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ * card_id TEXT Card id. [required] │
Expand Down Expand Up @@ -2447,6 +2446,11 @@ Usage: pipefy member remove [OPTIONS]

Remove users from a pipe.

Reads members back after the mutation. Output includes ``data`` (mutation
result) and ``warning`` (null when every requested user is gone). A warning
string means a requested user is still present; org-level permissions can
override pipe-level removal.

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ * --pipe TEXT Pipe id. [required] │
│ * --user-ids TEXT Comma-separated Pipefy user ids or UUIDs to │
Expand Down
Loading
Loading