docs(pylon): document the Pylon datasource for the Ruby agent - #30
docs(pylon): document the Pylon datasource for the Ruby agent#30christophebrun-forest wants to merge 5 commits into
Conversation
The `forest_admin_datasource_pylon` gem landed in agent-ruby#369: five collections over the Pylon API, cursor pagination, per-endpoint rate limiting, custom-field introspection, CRUD writes and two action plugins. Two pages, mirroring how the Zendesk connector is documented: - `get-started/connect/data-sources/pylon` — installation, the configuration options and their defaults, the collections and their relations, the embedded conversation thread, custom-field mapping, then what the API cannot do and what the datasource does about it: the filter allow-list per collection, the primary-key short-circuit and its caps, the absence of any sort parameter, why only the two fetch-all collections aggregate, the write budget, the rate-limit / retry trade and the error classes. - `product/process/advanced-concepts/plugins/pylon` — `CloseIssue` and `CreateIssueWithNotification`: options, form fields, the wizard, which scope bounds a close, and the batch cap. Ruby-only pages rather than tabbed ones: there is no Node.js Pylon connector, so a `Node.js` tab would have nothing to say. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
| @agent.collection :Customer do |collection| | ||
| collection.use( | ||
| ForestAdminDatasourcePylon::Plugins::CreateIssueWithNotification, | ||
| datasource: pylon_datasource, | ||
| sender_email: 'support@acme.com' | ||
| ) | ||
| end | ||
|
|
||
| @agent.collection :Order do |collection| |
There was a problem hiding this comment.
🟠 High plugins/pylon.mdx:23
The snippet calls collection on the uninitialized @agent, so copying it raises a NoMethodError and registers neither plugin action. The documented factory setup stores the agent in @create_agent; use that receiver for both collection declarations.
-@agent.collection :Customer do |collection|
+@create_agent.collection :Customer do |collection|
collection.use(
ForestAdminDatasourcePylon::Plugins::CreateIssueWithNotification,
datasource: pylon_datasource,
sender_email: 'support@acme.com'
)
end
-@agent.collection :Order do |collection|
+@create_agent.collection :Order do |collection|🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @product/process/advanced-concepts/plugins/pylon.mdx around lines 23-31:
The snippet calls `collection` on the uninitialized `@agent`, so copying it raises a `NoMethodError` and registers neither plugin action. The documented factory setup stores the agent in `@create_agent`; use that receiver for both collection declarations.
There was a problem hiding this comment.
Fixed in b79d271, but not the way the suggestion proposes — the diagnosis was off by one line.
@agent.collection is a valid call: AgentFactory includes ForestAdminDatasourceCustomizer::DSL::DatasourceHelpers, which defines collection(name, &block) as a wrapper over customize_collection. So the receiver is not the problem.
The actual defect was on line 21, which called add_datasource and threw away its return value, leaving @agent unbound two lines later. add_datasource returns self, so binding it is the whole fix:
@agent = ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(pylon_datasource, {})Switching to @create_agent instead would have made this the only plugins page using that name — plugins/zendesk.mdx and plugins/active-storage.mdx both use @agent. @create_agent is the datasource-page convention, which is why data-sources/pylon.mdx uses it: there the snippet shows the whole CreateAgent.setup! body, where that is the real local name.
| The Pylon connector is only available for Ruby (gem `forest_admin_datasource_pylon`). | ||
| </Warning> | ||
|
|
||
| Both plugins require the [Pylon datasource](/get-started/connect/data-sources/pylon) to be registered on your back-end: they need the `Datasource` instance to reach the Pylon API client. |
There was a problem hiding this comment.
🟡 Medium plugins/pylon.mdx:12
The page incorrectly tells users that both plugins require the Datasource to be registered with the agent, even though the actions use only the supplied datasource: object and its client; users who only need these actions are therefore steered into unnecessary datasource registration. Document that a constructed Datasource passed via datasource: is sufficient.
- Both plugins require the [Pylon datasource](/get-started/connect/data-sources/pylon) to be registered on your back-end: they need the `Datasource` instance to reach the Pylon API client.
+ Both plugins require a constructed `Datasource` instance passed via the `datasource:` option so they can reach the Pylon API client; they do not require that datasource to be registered with the agent.🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @product/process/advanced-concepts/plugins/pylon.mdx around line 12:
The page incorrectly tells users that both plugins require the `Datasource` to be registered with the agent, even though the actions use only the supplied `datasource:` object and its `client`; users who only need these actions are therefore steered into unnecessary datasource registration. Document that a constructed `Datasource` passed via `datasource:` is sufficient.
There was a problem hiding this comment.
Correct on the substance, and deliberately left as is in this PR.
Both plugins reach Pylon through options[:datasource].client only — CreateIssueWithNotification#create_issue and CloseIssue#apply_state — and never look the datasource up on the agent. A Datasource.new(api_key: ...) that is never passed to add_datasource builds its Client and is enough for both actions, so "must be registered" does overstate it.
The reason it stays: that sentence is copied verbatim from the Ruby tab of plugins/zendesk.mdx, where the same thing is true of CreateTicketWithNotification / CloseTicket. Relaxing it on the Pylon page alone would leave the two connectors documented as having different requirements when they have the same one, which is more confusing than the overstatement.
Worth its own PR that fixes both pages together. One thing to weigh there: constructing a Pylon Datasource runs the custom-field introspection (three GET /custom-fields calls in front of the Rails boot), so an unregistered instance kept only to feed the plugins pays that cost for a schema nobody reads — which is a reason to keep recommending registration even once the wording no longer requires it.
There was a problem hiding this comment.
Filed as EXT-24 so this does not stay a comment.
Checking Zendesk before writing it up turned the misalignment into a three-way one, and worse than described above. The Node tab of plugins/zendesk.mdx already gets it right — plugins accept a built client or raw credentials, and line 26 documents the no-datasource path outright ("when you install a single plugin on a project that does not register the Zendesk datasource"). The Ruby tab of that same page, four lines below, rules it out: "require the Zendesk datasource to be registered". So the Pylon page did not introduce the overstatement, it inherited it — and Node contradicts both.
Verified the Zendesk Ruby plugins behave exactly like the Pylon ones: CloseTicket#apply_status and CreateTicketWithNotification#executor reach Zendesk through options[:datasource].client and never look the datasource up on the agent.
Which makes the ticket a parity question rather than a wording one: Ruby has no equivalent of Node's "pass the credentials straight to the plugin", so a plugins-only customer must construct a full datasource regardless — and for Pylon that costs the three GET /custom-fields of the boot introspection for a schema nobody reads. Letting the Ruby plugins take a client: would fix the docs sentence and that cost in one go. Both options are in the ticket; the decision changes what the pages should say, so it wants making before either page is touched.
There was a problem hiding this comment.
Thanks for tracking this in EXT-24. Once the API/docs direction is decided, would you like me to prepare the follow-up PR against main?
Review of #30 against the merged code at cf20168a. Five places where the pages said something the package does not do. - `GET /me` was introduced as "the health check", which reads as a boot-time validation of the token. `Client#me` is called nowhere: the only boot call is `GET /custom-fields`, which degrades to the native schema on failure. A wrong api_key therefore boots the agent and fails on the first list. Say that, and point at `client.me` as the check to run yourself. - "Two relations Pylon's shape does not allow" covered one reason for two relations. Team membership is a shape limitation; `PylonAccount.owner_id` is not — the embedder would resolve it like any other key, and it is left a plain column because nothing in the panel asks for it yet. Split the two. - The plugins snippet called `add_datasource` without binding its return, then used `@agent` on the next line. Bind it. `@agent.collection` is valid — `AgentFactory` includes `DSL::DatasourceHelpers` — and is what the Zendesk plugins page already uses, so only the assignment was missing. - The `destination` row listed `internal` among the channels the first message is delivered through. It is the absence of a delivery: `Payload#build` omits `destination_metadata` entirely for it. The page had this right two paragraphs above and wrong in the table. - The filter tables list no `NOT_EQUAL`, because `operator_maps.rb` deliberately declares only `not_in` — the toolkit republishes the other spelling from it. The tables transcribe the wire maps, so the UI offers a filter the page reads as unavailable. One note under the tabs, next to the presence rewrite it shares a mechanism with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to b79d271, which overcorrected: it said nothing calls Pylon at boot and that the token is checked by the first request needing it. The custom-field introspection does call Pylon at boot and does carry the token -- `fetch_custom_fields` wraps `must_succeed` in `best_effort(default: nil)`, so a 401 there returns nil, logs a warning and leaves the agent on the native schema. The call happens; it just does not validate. Worth the precision because it is where the operator looks: a wrong key leaves a custom-fields warning in the boot log, which the Note further down already describes, and nothing else until the first list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review of Sequencing — three passages document the intended id-lookup behaviour, not the shipped one. The silent-truncation fix being in progress on agent-ruby, these describe what the code will do, not what 1.41.0 does:
Simplest path: merge this PR after the agent-ruby fix lands — the three passages become true as written. Inaccuracies to fix in this PR:
Minor: |
Review of d727d68 against the shipped code (agent-ruby origin/main, the Pylon package untouched since cf20168a). Nine places where the pages said something the package does not do, or said it in the wrong direction. Two of them sent a customer down a path that cannot work: - "To reach an account by an external id, use it in place of the primary key" is exactly what the collection is built to refuse. `GET /accounts/{id}` does accept one, but the serializer keeps Pylon's UUID as `record['id']` and `matches_id?` keeps a record only when that matches the id asked for -- per its own comment at `base_collection.rb:225-241`. With a scope the `and` goes through `/accounts/search`, which matches the UUID only. Either way the answer is empty. There is no Forest path to an account by external id; say that instead. (The stale comment at `account/api_filters.rb:28-29` is where this came from and wants fixing on the agent-ruby side too.) - "or referenced in a custom action", of the `messages` thread: a custom action can never receive it. `ActionContext#get_records` validates `fields` then calls `list` with an empty `Projection.new` regardless, and the embed's only trigger is `want_messages?(projection)`. Drop the half-sentence and say why `context.get_record(['messages'])` comes back without the column. Then the account/contact routing the first of those depends on: "need none of this" was true of the cap and false of the route -- a bare `id equals X` is still read through `GET /accounts/{id}`, to spend the cheaper budget rather than because the search could not answer it. That is the route that swallows the external id, so the page has to name it. Four corrections of substance: - The `PRESENT` / `BLANK` / `MISSING` rewrite was described backwards. The fields carrying the family declare it natively; the derivation puts it in front of every equality or membership field, including the ones that cannot answer it, which is where the translator refuses. Reworded in the direction `condition_tree_translator.rb:102-108` reads. - Custom fields: a `String` advertises the whole substring set, negations included (`BASE_OPS + FULL_TEXT.keys`), and a Date/Dateonly drops `IN` / `NOT_IN` on the way (`TIME_OPS`). The page said "substring" and omitted the loss. - The verb restriction gates the 5xx retries as much as the transport failures: faraday-retry ORs `methods` with `retry_if`, and `retry_if` only passes 429, so a `502` on `POST /issues/search` is not replayed. The page implied only dropped connections were verb-restricted. - Pylon was missing from the "All integrations" catalog, which declares itself exhaustive and held only the Zendesk card. And two cosmetic alignments on the repo's own precedent: the error table carried unqualified class names, where line 40 of the same page and `zendesk.mdx` qualify -- a copied `rescue APIError` is a `NameError`, so the namespace is stated once above the table; and the template tokens are spelled `{{record.field}}` like the Zendesk Ruby tab rather than spaced (`TOKEN_RE` tolerates both, this is consistency only). `5 000` / `1 000` were the only space-grouped figures in the docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three passages documented the intended primary-key-lookup behaviour rather than the shipped one. The silent-truncation fix is not on agent-ruby main -- the Pylon package has not moved since cf20168a, and no PR is open for it -- so the pages describe something no released gem does. What 1.41/1.42 actually do: `page_of_ids` takes the window off the ids, then caps at `MAX_ID_LOOKUPS`. `translate_page(nil)` returns `[0, nil]`, so a filter carrying no page has no window to take and the cap truncates the whole selection at twenty, with a `logger.warn`. And `refuse_wide_lookup` is reached only when `lookup.residual` is non-nil. - The datasource page said a wider selection "is read a page at a time rather than truncated at its first twenty". True of a paginated read; a pageless one -- every action's own read, and any API caller sending no page -- is exactly the truncation the sentence ruled out. Split into the two cases. - `CloseIssue` said the run is refused before its first write past the cap. False today for an action mounted on PylonIssue over an unscoped selection: the ids are truncated to twenty before the guard can see the rest, so fifty selected issues close twenty and report "20 issues closed". Named as the exception it is, with the two forms that are exact -- `issue_id_field` on a host collection, and any selection a scope or a segment narrows, where the residual makes the datasource refuse. - The example of a datasource refusal travelling to the operator only exists when a residual rides along, so it is now a scoped selection. Kept as its own commit on purpose: when the agent-ruby fix lands, reverting this restores three passages that become true as written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — all nine findings check out against the shipped code, and all nine are fixed in 1d6bed6. Two of them mattered a lot more than the rest, and one of your conclusions I'd like to push back on. The two that sent a customer nowhere. The account-by-external-id sentence is the worst thing that was on either page: While fixing the first one I hit something the review didn't name, and it's why that fix was incomplete on its own: On sequencing — the diagnosis is right, the conclusion I've changed. All three passages are indeed false on 1.41/1.42: But "merge this PR after the agent-ruby fix lands" doesn't work as stated: the Pylon package hasn't moved since So b88a504 reworks the three passages to describe what 1.42 ships, as its own commit, and the One nuance on the tokens: Left as is: Two follow-ups on the agent-ruby side, neither in scope here: the stale comment at |
Why
agent-ruby#369 merged
forest_admin_datasource_pylon(released in agent-ruby 1.41.0). Nothing in the docs mentioned it, so the gem shipped with its package README as the only place a customer could learn how to configure it — or, more to the point, what the connector refuses to do and why.Pylon is a ticketing API, not a database, so most of the page is that second half: several things Forest asks for have no equivalent, and the datasource answers a
400naming the reason rather than something that looks right and is not. That is the behaviour a customer meets first and has no way to guess.What changed
Two new pages, mirroring how the Zendesk connector is documented (datasource page + plugins page):
get-started/connect/data-sources/pylon.mdxbase_url, both timeouts,retry_policy,rate_limiter: nil, theboot_*trio.messagesthread column: field-by-field shape, the lazy fetch, the 10-row cap, and why a capped row isnilrather than[].is_read_onlyflag rule, and the boot-time degradation.idshort-circuit andMAX_ID_LOOKUPS, the absence of any sort parameter, the cursor pagination caps, why onlyPylonUser/PylonTeamaggregate — and exactly — the write budget and the per-collection create-only / update-only fields.DEFAULT_MAX_INTERVAL: 12trade stated and the snippet to choose the other way; then the error-class table and logging.product/process/advanced-concepts/plugins/pylon.mdxCreateIssueWithNotification: options, form fields, the two-page templates wizard, the refusals at registration, and what the operator reads back.CloseIssue: options, the one-action-per-scope registration and its name-collision refusal, which scope bounds it, and the 20-issue batch cap.Both pages are Ruby-only, with a
<Warning>saying so, rather than tabbed: there is no Node.js Pylon connector, so aNode.jstab would have nothing in it.Also:
docs.jsonnav for both pages, and a Pylon entry in the "Available datasources" list ofget-started/connect/data-sources/overview.mdx.How it was written
Every figure, option name and default is transcribed from the merged code at
cf20168a, not from the PR description: the package README,configuration.rb,retry_policy.rb,rate_limits.rb, the five collections with theirschema_definition/api_filters,operator_maps.rb,base_collection.rb/fetch_all_collection.rb/writes.rb,custom_fields_introspector.rb,cursor_walker.rb, and both plugins.Two places where that mattered:
CloseIssuescope caveat is the corrected reading, the oneb6bd085blanded. Mounted onPylonIssue, a scope bounds exactly what the action closes — the ids are resolved through the host collection first, and the agent intersects the operator's scope into that filter. The caveat only applies to the host-collection form, where the id column is the authority. The earlier wording had it backwards, and telling an operator a scope buys them nothing when it buys them the whole selection is the one error worth not copying into the docs.MAX_ID_LOOKUPS, which bounds a page rather than a selection (a93474b1), so a wider selection is read a page at a time rather than truncated.Three limitations the PR filed on Linear rather than fixing are not documented as behaviour, since they are open tickets: EXT-21 (a non-positive page limit), EXT-22 (a nested
andcarrying anid— the page does state that this shape is refused, which is what EXT-22 would change) and EXT-23 (an aggregation applying an operation the column type cannot take).Note
Add Pylon datasource and plugins documentation for Ruby agent
CreateIssueWithNotificationandCloseIssueaction plugins, including options, batch limits, and partial-failure handlingMacroscope summarized b88a504.