From 579b514c4680174bb83bdc0865120eadce017f06 Mon Sep 17 00:00:00 2001 From: Moe Katib Date: Fri, 27 Mar 2026 13:29:53 -0700 Subject: [PATCH] Move platform flows from skills/ to flows/ and fix package name - Rename skills/ to flows/ (these are flows, not skills) - Rename SKILL.md to README.md in each folder - Fix install command: @anthropic-ai/one -> @withone/cli in all 28 READMEs - 28 platforms, 72 flows, no content changes beyond the above Co-Authored-By: Claude Opus 4.6 (1M context) --- flows/active-campaign/README.md | 129 +++++++ .../activecampaign-create-contact.flow.json | 98 ++++++ .../activecampaign-create-deal.flow.json | 85 +++++ .../activecampaign-search-contacts.flow.json | 69 ++++ flows/airtable/README.md | 75 ++++ .../airtable-create-records.flow.json | 62 ++++ flows/apollo/README.md | 268 +++++++++++++++ flows/apollo/apollo-enrich-org.flow.json | 50 +++ flows/apollo/apollo-enrich-person.flow.json | 79 +++++ flows/apollo/apollo-people-search.flow.json | 95 +++++ .../apollo-prospect-to-sequence.flow.json | 165 +++++++++ flows/apollo/apollo-search-orgs.flow.json | 100 ++++++ flows/asana/README.md | 199 +++++++++++ ...ana-create-project-with-sections.flow.json | 118 +++++++ flows/asana/asana-create-task.flow.json | 112 ++++++ .../asana/asana-list-project-tasks.flow.json | 62 ++++ flows/asana/asana-search-tasks.flow.json | 98 ++++++ flows/attio/README.md | 233 +++++++++++++ flows/attio/attio-add-note.flow.json | 69 ++++ flows/attio/attio-add-to-list.flow.json | 65 ++++ flows/attio/attio-create-task.flow.json | 68 ++++ flows/attio/attio-search-records.flow.json | 74 ++++ flows/attio/attio-upsert-company.flow.json | 68 ++++ flows/attio/attio-upsert-person.flow.json | 78 +++++ flows/cal/README.md | 119 +++++++ flows/cal/cal-create-booking.flow.json | 79 +++++ flows/cal/cal-list-bookings.flow.json | 58 ++++ flows/cal/cal-manage-event-types.flow.json | 79 +++++ flows/calendly/README.md | 95 +++++ .../calendly-get-event-invitees.flow.json | 56 +++ .../calendly-list-event-types.flow.json | 58 ++++ flows/calendly/calendly-list-events.flow.json | 74 ++++ flows/diffbot/README.md | 54 +++ .../diffbot/diffbot-extract-article.flow.json | 65 ++++ flows/github/README.md | 325 ++++++++++++++++++ flows/github/github-commit-history.flow.json | 88 +++++ flows/github/github-create-issue.flow.json | 82 +++++ flows/github/github-create-pr.flow.json | 84 +++++ flows/github/github-list-issues.flow.json | 137 ++++++++ flows/github/github-pr-review.flow.json | 142 ++++++++ flows/github/github-repo-search.flow.json | 111 ++++++ flows/github/github-webhook-setup.flow.json | 85 +++++ flows/gmail/README.md | 123 +++++++ flows/gmail/gmail-read-emails.flow.json | 110 ++++++ flows/gmail/gmail-send-email.flow.json | 111 ++++++ flows/google-calendar/README.md | 86 +++++ .../google-calendar-create-event.flow.json | 94 +++++ flows/google-docs/README.md | 60 ++++ .../google-docs-create-document.flow.json | 71 ++++ flows/google-drive/README.md | 73 ++++ .../google-drive-manage-files.flow.json | 95 +++++ flows/google-sheets/README.md | 77 +++++ .../google-sheets-append-rows.flow.json | 70 ++++ flows/hubspot/README.md | 112 ++++++ .../hubspot/hubspot-create-contact.flow.json | 83 +++++ .../hubspot/hubspot-search-contacts.flow.json | 75 ++++ .../hubspot-search-crm-objects.flow.json | 83 +++++ flows/jira/README.md | 249 ++++++++++++++ flows/jira/jira-add-comment.flow.json | 72 ++++ flows/jira/jira-create-issue.flow.json | 91 +++++ flows/jira/jira-get-issue.flow.json | 68 ++++ flows/jira/jira-search-issues.flow.json | 73 ++++ flows/jira/jira-transition-issue.flow.json | 86 +++++ flows/meet-geek/README.md | 61 ++++ .../meet-geek-get-transcript.flow.json | 51 +++ flows/netlify/README.md | 80 +++++ flows/netlify/netlify-deploy-site.flow.json | 67 ++++ flows/netlify/netlify-list-sites.flow.json | 34 ++ flows/notion/README.md | 134 ++++++++ flows/notion/notion-create-page.flow.json | 73 ++++ flows/notion/notion-query-database.flow.json | 72 ++++ flows/personal-ai/README.md | 55 +++ .../personal-ai-send-message.flow.json | 74 ++++ flows/postmark/README.md | 64 ++++ flows/postmark/postmark-send-email.flow.json | 99 ++++++ flows/scrape-do/README.md | 56 +++ flows/scrape-do/scrape-do-async-job.flow.json | 70 ++++ flows/serp-api/README.md | 69 ++++ .../serp-api/serp-api-google-search.flow.json | 75 ++++ flows/shippo/README.md | 111 ++++++ flows/shippo/shippo-create-label.flow.json | 60 ++++ flows/shippo/shippo-create-shipment.flow.json | 137 ++++++++ flows/shippo/shippo-track-package.flow.json | 56 +++ flows/stripe/README.md | 226 ++++++++++++ flows/stripe/stripe-create-webhook.flow.json | 68 ++++ .../stripe-get-checkout-sessions.flow.json | 84 +++++ .../stripe-get-connected-accounts.flow.json | 69 ++++ .../stripe-get-invoice-detail.flow.json | 56 +++ flows/stripe/stripe-get-invoices.flow.json | 84 +++++ .../stripe-get-payment-intent.flow.json | 61 ++++ .../stripe-get-payment-intents.flow.json | 69 ++++ flows/trello/README.md | 91 +++++ flows/trello/trello-manage-cards.flow.json | 120 +++++++ flows/vercel/README.md | 109 ++++++ .../vercel/vercel-create-deployment.flow.json | 80 +++++ flows/vercel/vercel-list-projects.flow.json | 58 ++++ flows/vercel/vercel-manage-env-vars.flow.json | 56 +++ flows/zendesk/README.md | 79 +++++ flows/zendesk/zendesk-create-ticket.flow.json | 73 ++++ .../zendesk/zendesk-search-tickets.flow.json | 54 +++ 100 files changed, 9207 insertions(+) create mode 100644 flows/active-campaign/README.md create mode 100644 flows/active-campaign/activecampaign-create-contact.flow.json create mode 100644 flows/active-campaign/activecampaign-create-deal.flow.json create mode 100644 flows/active-campaign/activecampaign-search-contacts.flow.json create mode 100644 flows/airtable/README.md create mode 100644 flows/airtable/airtable-create-records.flow.json create mode 100644 flows/apollo/README.md create mode 100644 flows/apollo/apollo-enrich-org.flow.json create mode 100644 flows/apollo/apollo-enrich-person.flow.json create mode 100644 flows/apollo/apollo-people-search.flow.json create mode 100644 flows/apollo/apollo-prospect-to-sequence.flow.json create mode 100644 flows/apollo/apollo-search-orgs.flow.json create mode 100644 flows/asana/README.md create mode 100644 flows/asana/asana-create-project-with-sections.flow.json create mode 100644 flows/asana/asana-create-task.flow.json create mode 100644 flows/asana/asana-list-project-tasks.flow.json create mode 100644 flows/asana/asana-search-tasks.flow.json create mode 100644 flows/attio/README.md create mode 100644 flows/attio/attio-add-note.flow.json create mode 100644 flows/attio/attio-add-to-list.flow.json create mode 100644 flows/attio/attio-create-task.flow.json create mode 100644 flows/attio/attio-search-records.flow.json create mode 100644 flows/attio/attio-upsert-company.flow.json create mode 100644 flows/attio/attio-upsert-person.flow.json create mode 100644 flows/cal/README.md create mode 100644 flows/cal/cal-create-booking.flow.json create mode 100644 flows/cal/cal-list-bookings.flow.json create mode 100644 flows/cal/cal-manage-event-types.flow.json create mode 100644 flows/calendly/README.md create mode 100644 flows/calendly/calendly-get-event-invitees.flow.json create mode 100644 flows/calendly/calendly-list-event-types.flow.json create mode 100644 flows/calendly/calendly-list-events.flow.json create mode 100644 flows/diffbot/README.md create mode 100644 flows/diffbot/diffbot-extract-article.flow.json create mode 100644 flows/github/README.md create mode 100644 flows/github/github-commit-history.flow.json create mode 100644 flows/github/github-create-issue.flow.json create mode 100644 flows/github/github-create-pr.flow.json create mode 100644 flows/github/github-list-issues.flow.json create mode 100644 flows/github/github-pr-review.flow.json create mode 100644 flows/github/github-repo-search.flow.json create mode 100644 flows/github/github-webhook-setup.flow.json create mode 100644 flows/gmail/README.md create mode 100644 flows/gmail/gmail-read-emails.flow.json create mode 100644 flows/gmail/gmail-send-email.flow.json create mode 100644 flows/google-calendar/README.md create mode 100644 flows/google-calendar/google-calendar-create-event.flow.json create mode 100644 flows/google-docs/README.md create mode 100644 flows/google-docs/google-docs-create-document.flow.json create mode 100644 flows/google-drive/README.md create mode 100644 flows/google-drive/google-drive-manage-files.flow.json create mode 100644 flows/google-sheets/README.md create mode 100644 flows/google-sheets/google-sheets-append-rows.flow.json create mode 100644 flows/hubspot/README.md create mode 100644 flows/hubspot/hubspot-create-contact.flow.json create mode 100644 flows/hubspot/hubspot-search-contacts.flow.json create mode 100644 flows/hubspot/hubspot-search-crm-objects.flow.json create mode 100644 flows/jira/README.md create mode 100644 flows/jira/jira-add-comment.flow.json create mode 100644 flows/jira/jira-create-issue.flow.json create mode 100644 flows/jira/jira-get-issue.flow.json create mode 100644 flows/jira/jira-search-issues.flow.json create mode 100644 flows/jira/jira-transition-issue.flow.json create mode 100644 flows/meet-geek/README.md create mode 100644 flows/meet-geek/meet-geek-get-transcript.flow.json create mode 100644 flows/netlify/README.md create mode 100644 flows/netlify/netlify-deploy-site.flow.json create mode 100644 flows/netlify/netlify-list-sites.flow.json create mode 100644 flows/notion/README.md create mode 100644 flows/notion/notion-create-page.flow.json create mode 100644 flows/notion/notion-query-database.flow.json create mode 100644 flows/personal-ai/README.md create mode 100644 flows/personal-ai/personal-ai-send-message.flow.json create mode 100644 flows/postmark/README.md create mode 100644 flows/postmark/postmark-send-email.flow.json create mode 100644 flows/scrape-do/README.md create mode 100644 flows/scrape-do/scrape-do-async-job.flow.json create mode 100644 flows/serp-api/README.md create mode 100644 flows/serp-api/serp-api-google-search.flow.json create mode 100644 flows/shippo/README.md create mode 100644 flows/shippo/shippo-create-label.flow.json create mode 100644 flows/shippo/shippo-create-shipment.flow.json create mode 100644 flows/shippo/shippo-track-package.flow.json create mode 100644 flows/stripe/README.md create mode 100644 flows/stripe/stripe-create-webhook.flow.json create mode 100644 flows/stripe/stripe-get-checkout-sessions.flow.json create mode 100644 flows/stripe/stripe-get-connected-accounts.flow.json create mode 100644 flows/stripe/stripe-get-invoice-detail.flow.json create mode 100644 flows/stripe/stripe-get-invoices.flow.json create mode 100644 flows/stripe/stripe-get-payment-intent.flow.json create mode 100644 flows/stripe/stripe-get-payment-intents.flow.json create mode 100644 flows/trello/README.md create mode 100644 flows/trello/trello-manage-cards.flow.json create mode 100644 flows/vercel/README.md create mode 100644 flows/vercel/vercel-create-deployment.flow.json create mode 100644 flows/vercel/vercel-list-projects.flow.json create mode 100644 flows/vercel/vercel-manage-env-vars.flow.json create mode 100644 flows/zendesk/README.md create mode 100644 flows/zendesk/zendesk-create-ticket.flow.json create mode 100644 flows/zendesk/zendesk-search-tickets.flow.json diff --git a/flows/active-campaign/README.md b/flows/active-campaign/README.md new file mode 100644 index 0000000..d719c5e --- /dev/null +++ b/flows/active-campaign/README.md @@ -0,0 +1,129 @@ +--- +name: active-campaign +description: | + ActiveCampaign integration flows for the One CLI. Manage contacts, deals, + and tags in your ActiveCampaign CRM with ready-to-run workflows. +triggers: + - "activecampaign" + - "active campaign" + - "create contact activecampaign" + - "search contacts activecampaign" + - "create deal activecampaign" + - "/active-campaign" +--- + +# ActiveCampaign Flows + +Ready-to-run workflows for ActiveCampaign via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add active-campaign # Connect your ActiveCampaign account +one --agent list # Find your connection key +``` + +## Discovery + +Creating deals requires pipeline (`group`) and `stage` IDs. Filtering contacts may need `listId` or `tagId`. Find them with: + +```bash +# List pipelines (to get group/pipeline IDs and their stages) +one --agent actions search active-campaign "list pipelines" +one --agent actions execute active-campaign + +# List tags (to get tagId) +one --agent actions search active-campaign "list tags" +one --agent actions execute active-campaign + +# List contact lists (to get listId) +one --agent actions search active-campaign "list lists" +one --agent actions execute active-campaign +``` + +## Flows + +### Create Contact + +Create a new contact with email, name, phone, custom fields, and an optional tag. + +```bash +one flow execute activecampaign-create-contact.flow.json \ + --input activeCampaignConnectionKey="" \ + --input email="alice@example.com" \ + --input firstName="Alice" \ + --input lastName="Smith" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `activeCampaignConnectionKey` | Yes | ActiveCampaign connection key | +| `email` | Yes | Contact email address | +| `firstName` | No | First name | +| `lastName` | No | Last name | +| `phone` | No | Phone number | +| `fieldValues` | No | Custom field values (array of `{field, value}`) | +| `tagId` | No | Tag ID to apply after creation | + +**What it does under the hood:** + +1. Builds contact payload with standard and custom fields +2. Creates the contact via `POST /api/3/contacts` +3. Optionally adds a tag via `POST /api/3/contactTags` + +### Create Deal + +Create a deal in the ActiveCampaign CRM pipeline. + +```bash +one flow execute activecampaign-create-deal.flow.json \ + --input activeCampaignConnectionKey="" \ + --input title="Enterprise License" \ + --input value=50000 \ + --input group="1" \ + --input stage="1" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `activeCampaignConnectionKey` | Yes | ActiveCampaign connection key | +| `title` | Yes | Deal title | +| `value` | No | Deal value in cents (default 0) | +| `currency` | No | 3-letter currency code (default 'usd') | +| `group` | Yes | Pipeline ID | +| `stage` | Yes | Stage ID within the pipeline | +| `owner` | No | Owner user ID | +| `contactId` | No | Contact ID to associate | +| `description` | No | Deal description | + +### Search Contacts + +Search and filter contacts by email, name, list, or tag. + +```bash +one flow execute activecampaign-search-contacts.flow.json \ + --input activeCampaignConnectionKey="" \ + --input email="alice@example.com" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `activeCampaignConnectionKey` | Yes | ActiveCampaign connection key | +| `email` | No | Filter by email | +| `search` | No | Free-text search term | +| `listId` | No | Filter by list ID | +| `tagId` | No | Filter by tag ID | +| `limit` | No | Results to return (max 100, default 20) | + +## Adapting These Flows + +- **Tag + deal pipeline**: Chain `activecampaign-create-contact` into `activecampaign-create-deal` using the returned contact ID. +- **Bulk import**: Use the ActiveCampaign bulk import action for large contact lists. +- **Lead scoring**: Search contacts, then update scores via the update contact action. diff --git a/flows/active-campaign/activecampaign-create-contact.flow.json b/flows/active-campaign/activecampaign-create-contact.flow.json new file mode 100644 index 0000000..c3ee884 --- /dev/null +++ b/flows/active-campaign/activecampaign-create-contact.flow.json @@ -0,0 +1,98 @@ +{ + "key": "activecampaign-create-contact", + "name": "Create a Contact in ActiveCampaign", + "description": "Create a new contact in ActiveCampaign with email, name, phone, and custom fields. Optionally tag the contact after creation.", + "version": "1", + "inputs": { + "activeCampaignConnectionKey": { + "type": "string", + "required": true, + "description": "ActiveCampaign connection key", + "connection": { "platform": "active-campaign" } + }, + "email": { + "type": "string", + "required": true, + "description": "Contact email address" + }, + "firstName": { + "type": "string", + "required": false, + "description": "First name" + }, + "lastName": { + "type": "string", + "required": false, + "description": "Last name" + }, + "phone": { + "type": "string", + "required": false, + "description": "Phone number" + }, + "fieldValues": { + "type": "array", + "required": false, + "description": "Custom field values. Array of {field: fieldId, value: fieldValue} objects." + }, + "tagId": { + "type": "string", + "required": false, + "description": "Tag ID to apply to the contact after creation" + } + }, + "steps": [ + { + "id": "buildContact", + "name": "Build contact payload", + "type": "code", + "code": { + "source": "const { email, firstName, lastName, phone, fieldValues } = $.input;\nif (!email) throw new Error('email is required');\nconst contact = { email };\nif (firstName) contact.firstName = firstName;\nif (lastName) contact.lastName = lastName;\nif (phone) contact.phone = phone;\nif (fieldValues && fieldValues.length > 0) contact.fieldValues = fieldValues;\nreturn { contact };" + } + }, + { + "id": "createContact", + "name": "Create contact via ActiveCampaign API", + "type": "action", + "action": { + "platform": "active-campaign", + "actionId": "conn_mod_def::GJzzagLoQHU::D64WeRT1TmmUoyaJJ5t0IQ", + "connectionKey": "$.input.activeCampaignConnectionKey", + "data": "$.steps.buildContact.output" + } + }, + { + "id": "prepareTag", + "name": "Check if tag should be applied", + "type": "code", + "code": { + "source": "const tagId = $.input.tagId;\nconst contactId = $.steps.createContact.response?.contact?.id;\nreturn { shouldTag: !!(tagId && contactId), contactId: contactId || '', tagId: tagId || '' };" + } + }, + { + "id": "addTag", + "name": "Add tag to contact", + "type": "action", + "if": "$.steps.prepareTag.output.shouldTag", + "action": { + "platform": "active-campaign", + "actionId": "conn_mod_def::GJzz1X7lVnA::mVcMrPArQXaEMz_uM-ecUA", + "connectionKey": "$.input.activeCampaignConnectionKey", + "data": { + "contactTag": { + "contact": "$.steps.prepareTag.output.contactId", + "tag": "$.steps.prepareTag.output.tagId" + } + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createContact.response || {};\nconst contact = resp.contact || {};\nconst tagged = $.steps.addTag?.response?.contactTag ? true : false;\nreturn {\n contactId: contact.id || '',\n email: contact.email || $.input.email,\n firstName: contact.firstName || '',\n lastName: contact.lastName || '',\n created: !!contact.id,\n tagged,\n summary: contact.id\n ? `Created contact ${contact.email || $.input.email}${tagged ? ' (tagged)' : ''}`\n : 'Failed to create contact'\n};" + } + } + ] +} diff --git a/flows/active-campaign/activecampaign-create-deal.flow.json b/flows/active-campaign/activecampaign-create-deal.flow.json new file mode 100644 index 0000000..01762a4 --- /dev/null +++ b/flows/active-campaign/activecampaign-create-deal.flow.json @@ -0,0 +1,85 @@ +{ + "key": "activecampaign-create-deal", + "name": "Create a Deal in ActiveCampaign", + "description": "Create a new deal in ActiveCampaign CRM with title, value, pipeline, stage, and contact association.", + "version": "1", + "inputs": { + "activeCampaignConnectionKey": { + "type": "string", + "required": true, + "description": "ActiveCampaign connection key", + "connection": { "platform": "active-campaign" } + }, + "title": { + "type": "string", + "required": true, + "description": "Deal title" + }, + "value": { + "type": "number", + "required": false, + "default": 0, + "description": "Deal value in cents (e.g., 10000 = $100.00)" + }, + "currency": { + "type": "string", + "required": false, + "default": "usd", + "description": "3-letter currency code (e.g., 'usd')" + }, + "group": { + "type": "string", + "required": true, + "description": "Pipeline ID (called 'group' in the API)" + }, + "stage": { + "type": "string", + "required": true, + "description": "Stage ID within the pipeline" + }, + "owner": { + "type": "string", + "required": false, + "description": "Deal owner user ID" + }, + "contactId": { + "type": "string", + "required": false, + "description": "Contact ID to associate with the deal" + }, + "description": { + "type": "string", + "required": false, + "description": "Deal description" + } + }, + "steps": [ + { + "id": "buildDeal", + "name": "Build deal payload", + "type": "code", + "code": { + "source": "const { title, value, currency, group, stage, owner, contactId, description } = $.input;\nif (!title || !group || !stage) throw new Error('title, group (pipeline ID), and stage are required');\nconst deal = { title, group, stage };\ndeal.value = value || 0;\ndeal.currency = currency || 'usd';\nif (owner) deal.owner = owner;\nif (contactId) deal.contact = contactId;\nif (description) deal.description = description;\nreturn { deal };" + } + }, + { + "id": "createDeal", + "name": "Create deal via ActiveCampaign API", + "type": "action", + "action": { + "platform": "active-campaign", + "actionId": "conn_mod_def::GJzzlr7LEzc::T-RiAlnkS1G_sapuArW0cg", + "connectionKey": "$.input.activeCampaignConnectionKey", + "data": "$.steps.buildDeal.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createDeal.response || {};\nconst deal = resp.deal || {};\nreturn {\n dealId: deal.id || '',\n title: deal.title || $.input.title,\n value: deal.value || 0,\n currency: deal.currency || 'usd',\n stage: deal.stage || '',\n created: !!deal.id,\n summary: deal.id ? `Created deal \"${deal.title || $.input.title}\"` : 'Failed to create deal'\n};" + } + } + ] +} diff --git a/flows/active-campaign/activecampaign-search-contacts.flow.json b/flows/active-campaign/activecampaign-search-contacts.flow.json new file mode 100644 index 0000000..89172f6 --- /dev/null +++ b/flows/active-campaign/activecampaign-search-contacts.flow.json @@ -0,0 +1,69 @@ +{ + "key": "activecampaign-search-contacts", + "name": "Search Contacts in ActiveCampaign", + "description": "Search, list, and filter contacts in ActiveCampaign by email, name, tag, list, or custom query parameters.", + "version": "1", + "inputs": { + "activeCampaignConnectionKey": { + "type": "string", + "required": true, + "description": "ActiveCampaign connection key", + "connection": { "platform": "active-campaign" } + }, + "email": { + "type": "string", + "required": false, + "description": "Filter by email address" + }, + "search": { + "type": "string", + "required": false, + "description": "Search term to match against name, email, or other fields" + }, + "listId": { + "type": "string", + "required": false, + "description": "Filter by list ID" + }, + "tagId": { + "type": "string", + "required": false, + "description": "Filter by tag ID" + }, + "limit": { + "type": "number", + "required": false, + "default": 20, + "description": "Number of contacts to return (max 100)" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { email, search, listId, tagId, limit } = $.input;\nconst params = { limit: String(Math.min(limit || 20, 100)) };\nif (email) params.email = email;\nif (search) params.search = search;\nif (listId) params.listid = listId;\nif (tagId) params.tagid = tagId;\nreturn { params };" + } + }, + { + "id": "searchContacts", + "name": "Search contacts via ActiveCampaign API", + "type": "action", + "action": { + "platform": "active-campaign", + "actionId": "conn_mod_def::GJzzbG2HP0U::-KANfSK3RtSbF1PHyI_6dQ", + "connectionKey": "$.input.activeCampaignConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.searchContacts.response || {};\nconst contacts = resp.contacts || [];\nreturn {\n contacts: contacts.map(c => ({\n id: c.id,\n email: c.email,\n firstName: c.firstName,\n lastName: c.lastName,\n phone: c.phone,\n createdDate: c.cdate,\n tags: c.tags || []\n })),\n count: contacts.length,\n summary: `Found ${contacts.length} contact${contacts.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/airtable/README.md b/flows/airtable/README.md new file mode 100644 index 0000000..e5d7248 --- /dev/null +++ b/flows/airtable/README.md @@ -0,0 +1,75 @@ +--- +name: airtable +description: | + Airtable integration flow for the One CLI. Create records in any Airtable + base with field mapping and batch support. +triggers: + - "airtable" + - "create record airtable" + - "add row airtable" + - "/airtable" +--- + +# Airtable Flows + +Ready-to-run workflows for Airtable via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add airtable # Connect your Airtable account +one --agent list # Find your connection key +``` + +## Discovery + +You need a `baseId` and table name before using the flows. Find them with: + +```bash +# List all bases in your Airtable account +one --agent actions search airtable "list bases" +one --agent actions execute airtable + +# List tables in a specific base +one --agent actions search airtable "list tables" +one --agent actions execute airtable \ + --path-vars '{"baseId":"appXXXXXXXXXX"}' +``` + +## Flows + +### Create Records + +Create one or more records in an Airtable base table (max 10 per request). + +```bash +one flow execute airtable-create-records.flow.json \ + --input airtableConnectionKey="" \ + --input baseId="appXXXXXXXXXX" \ + --input tableIdOrName="Contacts" \ + --input records='[{"fields":{"Name":"Alice","Email":"alice@example.com"}}]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `airtableConnectionKey` | Yes | Airtable connection key | +| `baseId` | Yes | Base ID (starts with 'app') | +| `tableIdOrName` | Yes | Table ID or name | +| `records` | Yes | Array of record objects with `fields` | + +**What it does under the hood:** + +1. Validates records array (max 10 per Airtable API limit) +2. Normalizes record format (wraps in `{fields: ...}` if needed) +3. Creates records via `POST /{baseId}/{tableIdOrName}` + +**Note:** Airtable limits batch creates to 10 records. For larger batches, split your array and call the flow multiple times. + +## Adapting These Flows + +- **Update records**: Swap the create action for the update record action with a record ID. +- **Log to Airtable**: Pipe output from any other flow into this one to create audit log rows. +- **Form submissions**: Combine with a webhook trigger to create Airtable records from form data. diff --git a/flows/airtable/airtable-create-records.flow.json b/flows/airtable/airtable-create-records.flow.json new file mode 100644 index 0000000..ced959d --- /dev/null +++ b/flows/airtable/airtable-create-records.flow.json @@ -0,0 +1,62 @@ +{ + "key": "airtable-create-records", + "name": "Create Records in Airtable", + "description": "Create one or more records in an Airtable base table. Handles field mapping and batch record creation.", + "version": "1", + "inputs": { + "airtableConnectionKey": { + "type": "string", + "required": true, + "description": "Airtable connection key", + "connection": { "platform": "airtable" } + }, + "baseId": { + "type": "string", + "required": true, + "description": "Airtable base ID (starts with 'app')" + }, + "tableIdOrName": { + "type": "string", + "required": true, + "description": "Table ID or name" + }, + "records": { + "type": "array", + "required": true, + "description": "Array of record objects, each with a 'fields' object. Example: [{\"fields\": {\"Name\": \"Alice\", \"Email\": \"alice@example.com\"}}]" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Validate and build request payload", + "type": "code", + "code": { + "source": "const { records } = $.input;\nif (!records || !Array.isArray(records) || records.length === 0) throw new Error('records must be a non-empty array');\nif (records.length > 10) throw new Error('Airtable allows max 10 records per request. Split into batches.');\nconst formatted = records.map(r => ({ fields: r.fields || r }));\nreturn { records: formatted };" + } + }, + { + "id": "createRecords", + "name": "Create records via Airtable API", + "type": "action", + "action": { + "platform": "airtable", + "actionId": "conn_mod_def::GJz_XidaPDk::SxO92xzKQEWMDdWGK1gUAQ", + "connectionKey": "$.input.airtableConnectionKey", + "pathVars": { + "baseId": "$.input.baseId", + "tableIdOrName": "$.input.tableIdOrName" + }, + "data": "$.steps.buildPayload.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createRecords.response || {};\nconst created = resp.records || [];\nreturn {\n records: created.map(r => ({ id: r.id, fields: r.fields, createdTime: r.createdTime })),\n count: created.length,\n summary: `Created ${created.length} record${created.length === 1 ? '' : 's'} in ${$.input.tableIdOrName}`\n};" + } + } + ] +} diff --git a/flows/apollo/README.md b/flows/apollo/README.md new file mode 100644 index 0000000..d572307 --- /dev/null +++ b/flows/apollo/README.md @@ -0,0 +1,268 @@ +--- +name: apollo +description: | + Apollo.io integration flows for the One CLI. Ready-to-run workflows for + prospecting, enrichment, company research, and outreach sequence management + against Apollo's 270M+ contact database. +triggers: + - "search people" + - "find prospects" + - "enrich person" + - "enrich company" + - "enrich organization" + - "search companies" + - "search organizations" + - "prospect to sequence" + - "add to sequence" + - "apollo" + - "/apollo" +--- + +# Apollo Flows + +Ready-to-run workflows for [Apollo.io](https://apollo.io) via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add apollo # Connect your Apollo account +one --agent list # Find your connection key +``` + +## Discovery + +The prospect-to-sequence flow requires a `sequenceId` and `emailAccountId`. Find them with: + +```bash +# List your sequences (emailer campaigns) +one --agent actions search apollo "list sequences" +one --agent actions execute apollo \ + --query-params '{"per_page":"10"}' + +# List your email accounts +one --agent actions search apollo "list email accounts" +one --agent actions execute apollo +``` + +## Flows + +### People Search (Net-New Prospecting) + +Search Apollo's database of 270M+ people by title, seniority, location, employer +domain, technologies, and more. Does not return emails/phones -- use enrichment +for contact details. + +```bash +one flow execute apollo-people-search.flow.json \ + --input apolloConnectionKey="" \ + --input personTitles='["VP of Engineering", "CTO"]' \ + --input personSeniorities='["vp", "c_suite"]' \ + --input personLocations='["california"]' \ + --input employeeRanges='["50,500"]' \ + --input perPage=25 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `apolloConnectionKey` | Yes | Your Apollo connection key | +| `personTitles` | No | Job titles to search (matches similar titles) | +| `personSeniorities` | No | Levels: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern | +| `personLocations` | No | Where people live (cities, states, countries) | +| `organizationDomains` | No | Employer domains (up to 1000) | +| `organizationLocations` | No | Employer HQ locations | +| `employeeRanges` | No | Headcount ranges as "min,max" strings | +| `technologies` | No | Technologies used by employer (underscores for spaces) | +| `keywords` | No | Free-text keyword filter | +| `page` | No | Page number, 1-500 (default 1) | +| `perPage` | No | Results per page, 1-100 (default 25) | + +**What it does under the hood:** + +1. Builds array query parameters from structured inputs (Apollo uses bracket notation) +2. Calls `POST /api/v1/mixed_people/api_search` with filters as query params +3. Extracts and formats person data with organization metadata + +--- + +### Enrich a Person + +Enrich a single person's data by email, name+domain, LinkedIn URL, or Apollo ID. +Returns title, employer, employment history, location, seniority, and engagement +likelihood. + +```bash +one flow execute apollo-enrich-person.flow.json \ + --input apolloConnectionKey="" \ + --input email="tim@apollo.io" +``` + +```bash +one flow execute apollo-enrich-person.flow.json \ + --input apolloConnectionKey="" \ + --input firstName="Tim" \ + --input lastName="Zheng" \ + --input domain="apollo.io" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `apolloConnectionKey` | Yes | Your Apollo connection key | +| `email` | No | Email address (strongest match signal) | +| `firstName` | No | First name (combine with lastName + domain) | +| `lastName` | No | Last name | +| `domain` | No | Employer domain (no www. or @) | +| `linkedinUrl` | No | LinkedIn profile URL | +| `apolloPersonId` | No | Apollo person ID from a previous search | +| `revealPersonalEmails` | No | Reveal personal emails (consumes credits, GDPR-restricted) | + +At least one identifier is required. Best results come from email or name+domain. + +**What it does under the hood:** + +1. Validates at least one identifier is provided +2. Calls `POST /api/v1/people/match` with identifiers as query params +3. Extracts person profile, organization data, and employment history + +--- + +### Enrich an Organization by Domain + +Get comprehensive company intelligence from a single domain. Returns industry, +revenue, employee count, funding history, technologies, departmental headcount, +and social links. + +```bash +one flow execute apollo-enrich-org.flow.json \ + --input apolloConnectionKey="" \ + --input domain="stripe.com" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `apolloConnectionKey` | Yes | Your Apollo connection key | +| `domain` | Yes | Company domain (no www. or @) | + +**What it does under the hood:** + +1. Cleans domain input (strips protocol, www., trailing slashes) +2. Calls `GET /api/v1/organizations/enrich?domain=...` +3. Extracts company profile, funding events, tech stack, and department headcounts + +--- + +### Search Organizations (Company Search) + +Search Apollo's company database by name, domain, location, employee count, +revenue, funding, technologies, and keyword tags. + +```bash +one flow execute apollo-search-orgs.flow.json \ + --input apolloConnectionKey="" \ + --input technologies='["salesforce", "hubspot"]' \ + --input employeeRanges='["50,500"]' \ + --input locations='["california", "new york"]' \ + --input perPage=25 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `apolloConnectionKey` | Yes | Your Apollo connection key | +| `name` | No | Organization name (partial match) | +| `domains` | No | Organization domains (up to 1000) | +| `locations` | No | HQ locations (cities, states, countries) | +| `excludeLocations` | No | Exclude HQ locations | +| `employeeRanges` | No | Headcount ranges as "min,max" strings | +| `revenueMin` | No | Minimum revenue (integer) | +| `revenueMax` | No | Maximum revenue (integer) | +| `technologies` | No | Technologies in use (underscores for spaces) | +| `keywords` | No | Keyword tags | +| `page` | No | Page number, 1-500 (default 1) | +| `perPage` | No | Results per page, 1-100 (default 25) | + +**What it does under the hood:** + +1. Builds bracket-notation query params from structured inputs +2. Calls `POST /api/v1/mixed_companies/search` with filters as query params +3. Extracts organization profiles with pagination metadata + +--- + +### Prospect People and Add to Sequence + +End-to-end outbound workflow. Searches for people matching your ICP, creates them +as contacts in your Apollo workspace (with deduplication), and enrolls them into +an outreach sequence. + +```bash +one flow execute apollo-prospect-to-sequence.flow.json \ + --input apolloConnectionKey="" \ + --input personTitles='["VP of Engineering"]' \ + --input personSeniorities='["vp", "director"]' \ + --input employeeRanges='["50,500"]' \ + --input technologies='["salesforce"]' \ + --input maxContacts=10 \ + --input sequenceId="" \ + --input emailAccountId="" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `apolloConnectionKey` | Yes | Your Apollo connection key | +| `personTitles` | No | Job titles to search | +| `personSeniorities` | No | Seniority levels | +| `personLocations` | No | Where people live | +| `organizationDomains` | No | Employer domains | +| `employeeRanges` | No | Headcount ranges | +| `technologies` | No | Technologies used by employer | +| `maxContacts` | No | Max people to process (1-100, default 25) | +| `sequenceId` | Yes | Apollo sequence (emailer campaign) ID | +| `emailAccountId` | Yes | Apollo email account ID to send from | + +**What it does under the hood:** + +1. Searches Apollo's people database with your ICP filters +2. Loops through results, creating each as an Apollo contact (dedup enabled) +3. Collects created contact IDs +4. Adds all contacts to the specified sequence in a single API call +5. Returns pipeline summary (found -> created -> enrolled) + +**Finding your sequence and email account IDs:** + +Use the One CLI to search for sequences: +```bash +one --agent actions execute apollo \ + conn_mod_def::GJz2BqHQQLU::bmUbbCglQh2KqfGP0IXpOA \ + \ + --query-params '{"q_name":"your sequence name","per_page":"5"}' +``` + +## Apollo API Notes + +- **Rate limits**: 600 calls/hour per endpoint on most plans. +- **People Search** does not return emails or phones. Use the Enrich Person flow to get contact details. +- **Enrichment consumes credits** on your Apollo plan. +- **Master API key** may be required for sequence and people search endpoints. +- **50,000 record display limit**: Search results cap at 500 pages of 100 results. Use tighter filters to narrow results. + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Bulk enrichment**: Replace single enrichment with the bulk endpoints (`/api/v1/people/bulk_match`, `/api/v1/organizations/bulk_enrich`). +- **CRM sync**: Chain any flow into a HubSpot, Salesforce, or Attio action to push enriched data to your CRM. +- **Lead scoring**: Add a code step after enrichment to score leads based on seniority, company size, or tech stack. +- **Email verification**: Filter enriched contacts by `emailStatus === 'verified'` before adding to sequences. +- **Slack alerts**: Pipe prospect-to-sequence results into a Slack notification for your sales team. + +The orchestration logic is in the flow's `code` steps. Read them to understand the parameter construction, pagination, and data extraction patterns -- then build your own variations. diff --git a/flows/apollo/apollo-enrich-org.flow.json b/flows/apollo/apollo-enrich-org.flow.json new file mode 100644 index 0000000..c7ad65b --- /dev/null +++ b/flows/apollo/apollo-enrich-org.flow.json @@ -0,0 +1,50 @@ +{ + "key": "apollo-enrich-org", + "name": "Enrich an Organization by Domain", + "description": "Get comprehensive company data from Apollo by domain. Returns industry, revenue, funding history, employee count, technologies used, departmental headcount, and more.", + "version": "1", + "inputs": { + "apolloConnectionKey": { + "type": "string", + "required": true, + "description": "Apollo connection key", + "connection": { "platform": "apollo" } + }, + "domain": { + "type": "string", + "required": true, + "description": "Company domain to enrich (no www. or @). Example: apollo.io" + } + }, + "steps": [ + { + "id": "validateDomain", + "name": "Validate and clean domain input", + "type": "code", + "code": { + "source": "let domain = $.input.domain.trim().toLowerCase();\ndomain = domain.replace(/^(https?:\\/\\/)?(www\\.)?/, '').replace(/\\/$/, '').replace(/^@/, '');\nif (!domain || !domain.includes('.')) throw new Error('Invalid domain. Provide a bare domain like \"apollo.io\"');\nreturn { domain };" + } + }, + { + "id": "enrichOrg", + "name": "Call Apollo Organization Enrichment API", + "type": "action", + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz1-vQqfEI::rFgxNc8wQFWyV5kX88UBpg", + "connectionKey": "$.input.apolloConnectionKey", + "queryParams": { + "domain": "$.steps.validateDomain.output.domain" + } + } + }, + { + "id": "formatResult", + "name": "Extract and format organization data", + "type": "code", + "code": { + "source": "const resp = $.steps.enrichOrg.response || {};\nconst org = resp.organization;\n\nif (!org) return { found: false, summary: 'No organization found for domain: ' + $.steps.validateDomain.output.domain };\n\nconst funding = (org.funding_events || []).map(f => ({\n type: f.type,\n date: f.date,\n amount: f.amount,\n currency: f.currency,\n investors: f.investors\n}));\n\nconst technologies = org.technology_names || [];\nconst deptHeadcount = org.departmental_head_count || {};\n\nreturn {\n found: true,\n organization: {\n id: org.id,\n name: org.name,\n domain: org.primary_domain,\n website: org.website_url,\n industry: org.industry,\n industries: org.industries || [],\n description: org.short_description || org.seo_description || '',\n employees: org.estimated_num_employees,\n revenue: org.annual_revenue_printed,\n revenueNumeric: org.annual_revenue,\n totalFunding: org.total_funding_printed,\n latestFundingStage: org.latest_funding_stage,\n latestFundingDate: org.latest_funding_round_date,\n foundedYear: org.founded_year,\n location: {\n address: org.raw_address,\n city: org.city,\n state: org.state,\n country: org.country,\n postalCode: org.postal_code\n },\n social: {\n linkedin: org.linkedin_url,\n twitter: org.twitter_url,\n facebook: org.facebook_url\n },\n keywords: org.keywords || []\n },\n funding,\n technologies,\n departmentHeadcount: deptHeadcount,\n summary: `${org.name} | ${org.industry || 'Unknown industry'} | ${org.estimated_num_employees || '?'} employees | ${org.annual_revenue_printed || '?'} revenue | ${org.latest_funding_stage || 'No funding data'}`\n};" + } + } + ] +} diff --git a/flows/apollo/apollo-enrich-person.flow.json b/flows/apollo/apollo-enrich-person.flow.json new file mode 100644 index 0000000..e7aa9b7 --- /dev/null +++ b/flows/apollo/apollo-enrich-person.flow.json @@ -0,0 +1,79 @@ +{ + "key": "apollo-enrich-person", + "name": "Enrich a Person", + "description": "Enrich a single person's data using Apollo's People Enrichment API. Match by email, name+domain, LinkedIn URL, or Apollo person ID. Returns title, employer, employment history, location, seniority, and more.", + "version": "1", + "inputs": { + "apolloConnectionKey": { + "type": "string", + "required": true, + "description": "Apollo connection key", + "connection": { "platform": "apollo" } + }, + "email": { + "type": "string", + "required": false, + "description": "Person's email address (strongest match signal)" + }, + "firstName": { + "type": "string", + "required": false, + "description": "First name (use with lastName and domain for best results)" + }, + "lastName": { + "type": "string", + "required": false, + "description": "Last name" + }, + "domain": { + "type": "string", + "required": false, + "description": "Employer domain (no www. or @). Example: apollo.io" + }, + "linkedinUrl": { + "type": "string", + "required": false, + "description": "LinkedIn profile URL" + }, + "apolloPersonId": { + "type": "string", + "required": false, + "description": "Apollo person ID (from a previous search)" + }, + "revealPersonalEmails": { + "type": "boolean", + "required": false, + "default": false, + "description": "Set true to reveal personal emails (consumes credits, blocked in GDPR regions)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build enrichment query parameters", + "type": "code", + "code": { + "source": "const i = $.input;\nconst params = {};\n\nif (i.email) params.email = i.email;\nif (i.firstName) params.first_name = i.firstName;\nif (i.lastName) params.last_name = i.lastName;\nif (i.domain) params.domain = i.domain;\nif (i.linkedinUrl) params.linkedin_url = i.linkedinUrl;\nif (i.apolloPersonId) params.id = i.apolloPersonId;\nif (i.revealPersonalEmails) params.reveal_personal_emails = 'true';\n\n// Validate at least one identifier is provided\nconst hasId = i.email || i.linkedinUrl || i.apolloPersonId || (i.firstName && i.domain) || (i.firstName && i.lastName);\nif (!hasId) throw new Error('Provide at least one identifier: email, linkedinUrl, apolloPersonId, or firstName+domain/lastName');\n\nreturn { params };" + } + }, + { + "id": "enrichPerson", + "name": "Call Apollo People Enrichment API", + "type": "action", + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz1-5Pqiyg::USYoxmHNRM2-z8l50tKI_w", + "connectionKey": "$.input.apolloConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Extract and format enriched person data", + "type": "code", + "code": { + "source": "const resp = $.steps.enrichPerson.response || {};\nconst p = resp.person;\n\nif (!p) return { found: false, summary: 'No match found. Try providing more identifiers (email, domain, LinkedIn URL).' };\n\nconst org = p.organization || {};\nconst employment = (p.employment_history || []).map(e => ({\n title: e.title,\n company: e.organization_name,\n current: e.current || false,\n startDate: e.start_date,\n endDate: e.end_date\n}));\n\nreturn {\n found: true,\n person: {\n id: p.id,\n name: p.name,\n firstName: p.first_name,\n lastName: p.last_name,\n title: p.title,\n headline: p.headline,\n email: p.email || null,\n emailStatus: p.email_status || null,\n linkedinUrl: p.linkedin_url || null,\n city: p.city,\n state: p.state,\n country: p.country,\n seniority: p.seniority,\n departments: p.departments || [],\n isLikelyToEngage: p.is_likely_to_engage || false\n },\n organization: {\n id: org.id,\n name: org.name,\n domain: org.primary_domain,\n industry: org.industry,\n employees: org.estimated_num_employees,\n revenue: org.annual_revenue_printed,\n linkedinUrl: org.linkedin_url\n },\n employment,\n summary: `Enriched: ${p.name} - ${p.title || 'Unknown title'} at ${org.name || 'Unknown company'}`\n};" + } + } + ] +} diff --git a/flows/apollo/apollo-people-search.flow.json b/flows/apollo/apollo-people-search.flow.json new file mode 100644 index 0000000..769423f --- /dev/null +++ b/flows/apollo/apollo-people-search.flow.json @@ -0,0 +1,95 @@ +{ + "key": "apollo-people-search", + "name": "Search for People (Net-New Prospecting)", + "description": "Search Apollo's database of 270M+ people by title, seniority, location, employer domain, technologies, and more. Returns demographic data without emails/phones (use enrichment flows to get contact details).", + "version": "1", + "inputs": { + "apolloConnectionKey": { + "type": "string", + "required": true, + "description": "Apollo connection key", + "connection": { "platform": "apollo" } + }, + "personTitles": { + "type": "array", + "required": false, + "description": "Job titles to search for (matches similar titles by default). Examples: ['sales development representative', 'marketing manager']" + }, + "personSeniorities": { + "type": "array", + "required": false, + "description": "Seniority levels. Allowed: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern" + }, + "personLocations": { + "type": "array", + "required": false, + "description": "Where people live (cities, states, countries). Examples: ['california', 'ireland']" + }, + "organizationDomains": { + "type": "array", + "required": false, + "description": "Employer domains (up to 1000). Examples: ['apollo.io', 'microsoft.com']" + }, + "organizationLocations": { + "type": "array", + "required": false, + "description": "Employer HQ locations. Examples: ['texas', 'tokyo']" + }, + "employeeRanges": { + "type": "array", + "required": false, + "description": "Employer headcount ranges as 'min,max' strings. Examples: ['1,10', '250,500']" + }, + "technologies": { + "type": "array", + "required": false, + "description": "Technologies used by employer (any match). Use underscores for spaces. Examples: ['salesforce', 'google_analytics']" + }, + "keywords": { + "type": "string", + "required": false, + "description": "Free-text keyword filter across results" + }, + "page": { + "type": "number", + "required": false, + "default": 1, + "description": "Page number (1-500)" + }, + "perPage": { + "type": "number", + "required": false, + "default": 25, + "description": "Results per page (1-100)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters from inputs", + "type": "code", + "code": { + "source": "const i = $.input;\nconst params = {};\nconst page = Math.max(1, Math.min(500, i.page || 1));\nconst perPage = Math.max(1, Math.min(100, i.perPage || 25));\nparams.page = String(page);\nparams.per_page = String(perPage);\n\nif (i.personTitles && i.personTitles.length) {\n i.personTitles.forEach((t, idx) => { params['person_titles[' + idx + ']'] = t; });\n}\nif (i.personSeniorities && i.personSeniorities.length) {\n i.personSeniorities.forEach((s, idx) => { params['person_seniorities[' + idx + ']'] = s; });\n}\nif (i.personLocations && i.personLocations.length) {\n i.personLocations.forEach((l, idx) => { params['person_locations[' + idx + ']'] = l; });\n}\nif (i.organizationDomains && i.organizationDomains.length) {\n i.organizationDomains.forEach((d, idx) => { params['q_organization_domains_list[' + idx + ']'] = d; });\n}\nif (i.organizationLocations && i.organizationLocations.length) {\n i.organizationLocations.forEach((l, idx) => { params['organization_locations[' + idx + ']'] = l; });\n}\nif (i.employeeRanges && i.employeeRanges.length) {\n i.employeeRanges.forEach((r, idx) => { params['organization_num_employees_ranges[' + idx + ']'] = r; });\n}\nif (i.technologies && i.technologies.length) {\n i.technologies.forEach((t, idx) => { params['currently_using_any_of_technology_uids[' + idx + ']'] = t; });\n}\nif (i.keywords) params.q_keywords = i.keywords;\n\nreturn { params, page, perPage };" + } + }, + { + "id": "searchPeople", + "name": "Search Apollo people database", + "type": "action", + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz2AmtsIH8::-KnLbk4nSHurCgEJXsodwA", + "connectionKey": "$.input.apolloConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResults", + "name": "Format and summarize results", + "type": "code", + "code": { + "source": "const resp = $.steps.searchPeople.response || {};\nconst people = (resp.people || []).map(p => ({\n id: p.id,\n firstName: p.first_name,\n lastName: p.last_name_obfuscated || '',\n title: p.title || '',\n hasEmail: p.has_email || false,\n hasPhone: p.has_direct_phone === 'Yes',\n organization: p.organization ? p.organization.name : '',\n location: [p.has_city && 'city', p.has_state && 'state', p.has_country && 'country'].filter(Boolean).length > 0\n}));\n\nconst totalEntries = resp.total_entries || 0;\nconst page = $.steps.buildParams.output.page;\nconst perPage = $.steps.buildParams.output.perPage;\n\nreturn {\n people,\n totalEntries,\n page,\n perPage,\n totalPages: Math.ceil(totalEntries / perPage),\n count: people.length,\n summary: totalEntries > 0\n ? `Found ${totalEntries.toLocaleString()} people (showing page ${page}, ${people.length} results)`\n : 'No people matched your search criteria'\n};" + } + } + ] +} diff --git a/flows/apollo/apollo-prospect-to-sequence.flow.json b/flows/apollo/apollo-prospect-to-sequence.flow.json new file mode 100644 index 0000000..f074be0 --- /dev/null +++ b/flows/apollo/apollo-prospect-to-sequence.flow.json @@ -0,0 +1,165 @@ +{ + "key": "apollo-prospect-to-sequence", + "name": "Prospect People and Add to Sequence", + "description": "End-to-end outbound workflow: search for people matching your ICP, create them as contacts in Apollo, and add them to an outreach sequence. Handles the full prospect -> contact -> sequence pipeline.", + "version": "1", + "inputs": { + "apolloConnectionKey": { + "type": "string", + "required": true, + "description": "Apollo connection key", + "connection": { "platform": "apollo" } + }, + "personTitles": { + "type": "array", + "required": false, + "description": "Job titles to search for. Examples: ['VP of Engineering', 'CTO']" + }, + "personSeniorities": { + "type": "array", + "required": false, + "description": "Seniority levels. Allowed: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern" + }, + "personLocations": { + "type": "array", + "required": false, + "description": "Where people live. Examples: ['california', 'new york']" + }, + "organizationDomains": { + "type": "array", + "required": false, + "description": "Employer domains. Examples: ['stripe.com', 'shopify.com']" + }, + "employeeRanges": { + "type": "array", + "required": false, + "description": "Employer headcount ranges. Examples: ['50,200', '200,1000']" + }, + "technologies": { + "type": "array", + "required": false, + "description": "Technologies used by employer. Examples: ['salesforce', 'hubspot']" + }, + "maxContacts": { + "type": "number", + "required": false, + "default": 25, + "description": "Maximum number of people to prospect and add (1-100)" + }, + "sequenceId": { + "type": "string", + "required": true, + "description": "Apollo sequence (emailer campaign) ID to add contacts to" + }, + "emailAccountId": { + "type": "string", + "required": true, + "description": "Apollo email account ID to send emails from" + } + }, + "steps": [ + { + "id": "buildSearchParams", + "name": "Build people search parameters", + "type": "code", + "code": { + "source": "const i = $.input;\nconst perPage = Math.max(1, Math.min(100, i.maxContacts || 25));\nconst params = { page: '1', per_page: String(perPage) };\n\nif (i.personTitles && i.personTitles.length) {\n i.personTitles.forEach((t, idx) => { params['person_titles[' + idx + ']'] = t; });\n}\nif (i.personSeniorities && i.personSeniorities.length) {\n i.personSeniorities.forEach((s, idx) => { params['person_seniorities[' + idx + ']'] = s; });\n}\nif (i.personLocations && i.personLocations.length) {\n i.personLocations.forEach((l, idx) => { params['person_locations[' + idx + ']'] = l; });\n}\nif (i.organizationDomains && i.organizationDomains.length) {\n i.organizationDomains.forEach((d, idx) => { params['q_organization_domains_list[' + idx + ']'] = d; });\n}\nif (i.employeeRanges && i.employeeRanges.length) {\n i.employeeRanges.forEach((r, idx) => { params['organization_num_employees_ranges[' + idx + ']'] = r; });\n}\nif (i.technologies && i.technologies.length) {\n i.technologies.forEach((t, idx) => { params['currently_using_any_of_technology_uids[' + idx + ']'] = t; });\n}\n\nreturn { params, perPage };" + } + }, + { + "id": "searchPeople", + "name": "Search Apollo for matching people", + "type": "action", + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz2AmtsIH8::-KnLbk4nSHurCgEJXsodwA", + "connectionKey": "$.input.apolloConnectionKey", + "queryParams": "$.steps.buildSearchParams.output.params" + } + }, + { + "id": "extractPeople", + "name": "Extract people from search results", + "type": "code", + "code": { + "source": "const resp = $.steps.searchPeople.response || {};\nconst people = resp.people || [];\nif (people.length === 0) throw new Error('No people found matching your criteria. Try broadening your search filters.');\nreturn { people, count: people.length, totalAvailable: resp.total_entries || 0 };" + } + }, + { + "id": "createContacts", + "name": "Create Apollo contacts from search results", + "type": "loop", + "loop": { + "over": "$.steps.extractPeople.output.people", + "as": "person", + "maxConcurrency": 3, + "steps": [ + { + "id": "createContact", + "name": "Create contact in Apollo", + "type": "action", + "onError": { "strategy": "continue" }, + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz18_qaRlg::Vlkwc7g2RQynN5-X7YAr7w", + "connectionKey": "$.input.apolloConnectionKey", + "data": { + "first_name": "$.loop.person.first_name", + "last_name": "$.loop.person.last_name", + "title": "$.loop.person.title", + "organization_name": "$.loop.person.organization.name", + "run_dedupe": true + } + } + }, + { + "id": "extractContactId", + "name": "Extract contact ID from response", + "type": "code", + "code": { + "source": "const resp = $.steps.createContact?.response;\nif (!resp || !resp.contact) return { contactId: null, skipped: true };\nreturn { contactId: resp.contact.id, name: resp.contact.name, skipped: false };" + } + } + ] + } + }, + { + "id": "collectContactIds", + "name": "Collect created contact IDs", + "type": "code", + "code": { + "source": "const iterations = $.steps.createContacts?.response?.iterations || [];\nconst contactIds = iterations\n .map(r => r?.extractContactId?.output)\n .filter(o => o && !o.skipped && o.contactId)\n .map(o => o.contactId);\n\nif (contactIds.length === 0) throw new Error('No contacts were created. Check your Apollo plan and API key permissions.');\n\nreturn { contactIds, count: contactIds.length };" + } + }, + { + "id": "buildSequenceParams", + "name": "Build sequence enrollment parameters", + "type": "code", + "code": { + "source": "const contactIds = $.steps.collectContactIds.output.contactIds;\nconst sequenceId = $.input.sequenceId;\nconst emailAccountId = $.input.emailAccountId;\n\nconst params = {\n emailer_campaign_id: sequenceId,\n send_email_from_email_account_id: emailAccountId,\n sequence_active_in_other_campaigns: 'true'\n};\ncontactIds.forEach((id, idx) => { params['contact_ids[' + idx + ']'] = id; });\n\nreturn { params, sequenceId };" + } + }, + { + "id": "addToSequence", + "name": "Add contacts to the outreach sequence", + "type": "action", + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz2A8rUZLQ::BRe1mfcBRQ2suX0rLoMxnA", + "connectionKey": "$.input.apolloConnectionKey", + "pathVars": { + "sequenceId": "$.input.sequenceId" + }, + "queryParams": "$.steps.buildSequenceParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Compile final summary", + "type": "code", + "code": { + "source": "const searchCount = $.steps.extractPeople.output.count;\nconst totalAvailable = $.steps.extractPeople.output.totalAvailable;\nconst contactsCreated = $.steps.collectContactIds.output.count;\nconst seqResp = $.steps.addToSequence.response || {};\nconst addedContacts = (seqResp.contacts || []).length;\nconst skipped = seqResp.skipped_contact_ids ? Object.keys(seqResp.skipped_contact_ids).length : 0;\nconst campaign = seqResp.emailer_campaign || {};\n\nreturn {\n pipeline: {\n peopleFound: totalAvailable,\n peopleProcessed: searchCount,\n contactsCreated,\n addedToSequence: addedContacts,\n skippedFromSequence: skipped\n },\n sequence: {\n id: campaign.id || $.input.sequenceId,\n name: campaign.name || 'Unknown',\n active: campaign.active || false\n },\n summary: `Prospected ${searchCount} people from ${totalAvailable.toLocaleString()} matches. Created ${contactsCreated} contacts. Added ${addedContacts} to sequence \"${campaign.name || 'Unknown'}\" (${skipped} skipped).`\n};" + } + } + ] +} diff --git a/flows/apollo/apollo-search-orgs.flow.json b/flows/apollo/apollo-search-orgs.flow.json new file mode 100644 index 0000000..7e978db --- /dev/null +++ b/flows/apollo/apollo-search-orgs.flow.json @@ -0,0 +1,100 @@ +{ + "key": "apollo-search-orgs", + "name": "Search Organizations (Company Search)", + "description": "Search Apollo's company database by name, domain, location, employee count, revenue, funding, technologies, and more. Returns paginated results with company metadata.", + "version": "1", + "inputs": { + "apolloConnectionKey": { + "type": "string", + "required": true, + "description": "Apollo connection key", + "connection": { "platform": "apollo" } + }, + "name": { + "type": "string", + "required": false, + "description": "Organization name (partial match supported). Example: 'apollo'" + }, + "domains": { + "type": "array", + "required": false, + "description": "Organization domains (up to 1000). Examples: ['apollo.io', 'microsoft.com']" + }, + "locations": { + "type": "array", + "required": false, + "description": "HQ locations (cities, states, countries). Examples: ['texas', 'tokyo']" + }, + "excludeLocations": { + "type": "array", + "required": false, + "description": "Exclude HQ locations. Examples: ['minnesota', 'ireland']" + }, + "employeeRanges": { + "type": "array", + "required": false, + "description": "Employee count ranges as 'min,max' strings. Examples: ['1,10', '250,500']" + }, + "revenueMin": { + "type": "number", + "required": false, + "description": "Minimum revenue (integer, no currency symbols). Example: 500000" + }, + "revenueMax": { + "type": "number", + "required": false, + "description": "Maximum revenue (integer, no currency symbols). Example: 50000000" + }, + "technologies": { + "type": "array", + "required": false, + "description": "Technologies in use. Use underscores for spaces. Examples: ['salesforce', 'google_analytics']" + }, + "keywords": { + "type": "array", + "required": false, + "description": "Keyword tags. Examples: ['mining', 'sales strategy']" + }, + "page": { + "type": "number", + "required": false, + "default": 1, + "description": "Page number (1-500)" + }, + "perPage": { + "type": "number", + "required": false, + "default": 25, + "description": "Results per page (1-100)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters from inputs", + "type": "code", + "code": { + "source": "const i = $.input;\nconst params = {};\nconst page = Math.max(1, Math.min(500, i.page || 1));\nconst perPage = Math.max(1, Math.min(100, i.perPage || 25));\nparams.page = String(page);\nparams.per_page = String(perPage);\n\nif (i.name) params.q_organization_name = i.name;\nif (i.domains && i.domains.length) {\n i.domains.forEach((d, idx) => { params['q_organization_domains_list[' + idx + ']'] = d; });\n}\nif (i.locations && i.locations.length) {\n i.locations.forEach((l, idx) => { params['organization_locations[' + idx + ']'] = l; });\n}\nif (i.excludeLocations && i.excludeLocations.length) {\n i.excludeLocations.forEach((l, idx) => { params['organization_not_locations[' + idx + ']'] = l; });\n}\nif (i.employeeRanges && i.employeeRanges.length) {\n i.employeeRanges.forEach((r, idx) => { params['organization_num_employees_ranges[' + idx + ']'] = r; });\n}\nif (i.revenueMin != null) params['revenue_range[min]'] = String(i.revenueMin);\nif (i.revenueMax != null) params['revenue_range[max]'] = String(i.revenueMax);\nif (i.technologies && i.technologies.length) {\n i.technologies.forEach((t, idx) => { params['currently_using_any_of_technology_uids[' + idx + ']'] = t; });\n}\nif (i.keywords && i.keywords.length) {\n i.keywords.forEach((k, idx) => { params['q_organization_keyword_tags[' + idx + ']'] = k; });\n}\n\nreturn { params, page, perPage };" + } + }, + { + "id": "searchOrgs", + "name": "Search Apollo organizations database", + "type": "action", + "action": { + "platform": "apollo", + "actionId": "conn_mod_def::GJz2AXYS8Hw::nYfwc352SyqMeSNf-v4kmQ", + "connectionKey": "$.input.apolloConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResults", + "name": "Format and summarize results", + "type": "code", + "code": { + "source": "const resp = $.steps.searchOrgs.response || {};\nconst orgs = (resp.organizations || []).map(o => ({\n id: o.id,\n name: o.name,\n domain: o.primary_domain,\n website: o.website_url,\n linkedin: o.linkedin_url,\n phone: o.phone,\n foundedYear: o.founded_year,\n alexaRanking: o.alexa_ranking\n}));\n\nconst pagination = resp.pagination || {};\nconst totalEntries = pagination.total_entries || 0;\nconst page = $.steps.buildParams.output.page;\nconst perPage = $.steps.buildParams.output.perPage;\n\nreturn {\n organizations: orgs,\n totalEntries,\n page,\n perPage,\n totalPages: pagination.total_pages || 0,\n count: orgs.length,\n summary: totalEntries > 0\n ? `Found ${totalEntries.toLocaleString()} organizations (showing page ${page}, ${orgs.length} results)`\n : 'No organizations matched your search criteria'\n};" + } + } + ] +} diff --git a/flows/asana/README.md b/flows/asana/README.md new file mode 100644 index 0000000..6ce805f --- /dev/null +++ b/flows/asana/README.md @@ -0,0 +1,199 @@ +--- +name: asana +description: | + Asana integration flows for the One CLI. Ready-to-run workflows that handle + the orchestration complexity (multi-step task creation with section placement, + two-step list+detail patterns, project scaffolding) so you don't have to. +triggers: + - "create task" + - "asana task" + - "list tasks" + - "project tasks" + - "create project" + - "search tasks" + - "asana" + - "/asana" +--- + +# Asana Flows + +Ready-to-run workflows for Asana via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add asana # Connect your Asana account +one --agent list # Find your connection key +``` + +## Discovery + +Most Asana flows require IDs that aren't obvious. Find them with: + +```bash +# List your workspaces (to get workspaceGid) +one --agent actions search asana "get workspaces" +one --agent actions execute asana + +# List projects in a workspace (to get projectGid) +one --agent actions search asana "get projects" +one --agent actions execute asana \ + --query-params '{"workspace":""}' + +# List sections in a project (to get sectionGid) +one --agent actions search asana "get sections" +one --agent actions execute asana \ + --path-vars '{"project_gid":""}' +``` + +## Flows + +### Create Task + +Creates a task with optional project and section assignment. Handles the +multi-step pattern where you first create the task, then move it to a +specific section within the project. + +```bash +one flow execute asana-create-task.flow.json \ + --input asanaConnectionKey="" \ + --input workspaceGid="" \ + --input name="Ship v2 release notes" \ + --input projectGid="" \ + --input sectionGid="" \ + --input assignee="me" \ + --input dueOn="2026-04-01" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `asanaConnectionKey` | Yes | Your Asana connection key | +| `name` | Yes | Task name | +| `workspaceGid` | Yes | Workspace GID | +| `projectGid` | No | Project GID to add the task to | +| `sectionGid` | No | Section GID to place the task in (requires projectGid) | +| `assignee` | No | Assignee GID or email address | +| `notes` | No | Task description (plain text) | +| `htmlNotes` | No | Task description (HTML, overrides notes) | +| `dueOn` | No | Due date (YYYY-MM-DD) | +| `tags` | No | Array of tag GIDs | +| `priority` | No | Priority level | + +**What it does under the hood:** + +1. Builds the task payload with workspace, project, assignee, and dates +2. Creates the task via `POST /tasks` +3. If `sectionGid` is provided, moves the task into that section via `POST /sections/{sectionGid}/addTask` + +### List Project Tasks + +Lists all tasks in a project with full details including assignee, due date, +completion status, section, and tags. + +```bash +one flow execute asana-list-project-tasks.flow.json \ + --input asanaConnectionKey="" \ + --input projectGid="" \ + --input completedSince="now" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `asanaConnectionKey` | Yes | Your Asana connection key | +| `projectGid` | Yes | Project GID to list tasks from | +| `completedSince` | No | Filter completed tasks (ISO 8601 date, or `"now"` to exclude completed) | +| `limit` | No | Max tasks to return (1-100, default 50) | + +**What it does under the hood:** + +1. Builds query params with opt_fields for full detail in a single request +2. Lists tasks via `GET /projects/{projectGid}/tasks` with expanded fields +3. Formats results with open/completed counts and section grouping + +### Create Project with Sections + +Creates a new project and sets up sections (columns for board view, groups +for list view). Handles the multi-step orchestration of creating the project +first, then adding each section sequentially. + +```bash +one flow execute asana-create-project-with-sections.flow.json \ + --input asanaConnectionKey="" \ + --input workspaceGid="" \ + --input name="Q2 Sprint Board" \ + --input layout="board" \ + --input sections='["Backlog", "Ready", "In Progress", "Review", "Done"]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `asanaConnectionKey` | Yes | Your Asana connection key | +| `name` | Yes | Project name | +| `workspaceGid` | Yes | Workspace GID | +| `teamGid` | No | Team GID (required for organization workspaces) | +| `layout` | No | `"board"` (kanban) or `"list"` (default: board) | +| `sections` | No | Array of section names (default: To Do, In Progress, Done) | +| `notes` | No | Project description | +| `color` | No | Project color (e.g., `"dark-green"`, `"dark-blue"`) | + +**What it does under the hood:** + +1. Creates the project via `POST /projects` with layout and metadata +2. Loops through section names sequentially (order matters) +3. Creates each section via `POST /projects/{projectGid}/sections` +4. Returns the project GID, permalink, and all created section GIDs + +### Search Tasks + +Searches for tasks across a workspace with filters for assignee, project, +completion status, due dates, and text. + +```bash +one flow execute asana-search-tasks.flow.json \ + --input asanaConnectionKey="" \ + --input workspaceGid="" \ + --input text="release notes" \ + --input completed=false \ + --input assignee="me" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `asanaConnectionKey` | Yes | Your Asana connection key | +| `workspaceGid` | Yes | Workspace GID to search within | +| `text` | No | Text to search in task names and descriptions | +| `assignee` | No | Assignee GID or `"me"` | +| `projectGid` | No | Scope search to a specific project | +| `completed` | No | `true` for completed, `false` for open, omit for both | +| `dueOnBefore` | No | Tasks due on or before this date (YYYY-MM-DD) | +| `dueOnAfter` | No | Tasks due on or after this date (YYYY-MM-DD) | +| `isBlocked` | No | Filter tasks blocked by dependencies | +| `sortBy` | No | Sort: `due_date`, `created_at`, `completed_at`, `likes`, `modified_at` (default) | +| `sortAscending` | No | Sort direction (default: false/descending) | + +**What it does under the hood:** + +1. Builds search query parameters from all provided filters +2. Searches via `GET /workspaces/{workspaceGid}/tasks/search` with opt_fields +3. Formats results with assignee names, sections, tags, and open/completed counts + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Sprint standup report**: Chain `asana-list-project-tasks` with a Slack message action to post a daily summary. +- **Bulk task creation**: Wrap `asana-create-task` in an outer loop to create multiple tasks from a CSV or spreadsheet. +- **Project templating**: Extend `asana-create-project-with-sections` to also create starter tasks in each section. +- **Overdue alerts**: Use `asana-search-tasks` with `dueOnBefore` set to today and `completed=false`, then pipe to Slack or email. +- **Cross-project dashboard**: Run `asana-list-project-tasks` across multiple projects and merge the results. + +The orchestration knowledge is in the flow's `code` steps. Read them to understand the payload construction, section placement, and search parameter patterns -- then build your own variations. diff --git a/flows/asana/asana-create-project-with-sections.flow.json b/flows/asana/asana-create-project-with-sections.flow.json new file mode 100644 index 0000000..b79145f --- /dev/null +++ b/flows/asana/asana-create-project-with-sections.flow.json @@ -0,0 +1,118 @@ +{ + "key": "asana-create-project-with-sections", + "name": "Create Project with Sections in Asana", + "description": "Creates a new Asana project and sets up sections (columns for board view, or groups for list view). Handles the multi-step orchestration: create project, then create each section sequentially.", + "version": "1", + "inputs": { + "asanaConnectionKey": { + "type": "string", + "required": true, + "description": "Asana connection key", + "connection": { "platform": "asana" } + }, + "name": { + "type": "string", + "required": true, + "description": "Project name" + }, + "workspaceGid": { + "type": "string", + "required": true, + "description": "Workspace GID where the project will be created" + }, + "teamGid": { + "type": "string", + "required": false, + "description": "Team GID to create the project under. Required for organization workspaces." + }, + "layout": { + "type": "string", + "required": false, + "default": "board", + "description": "Project layout: 'board' (kanban) or 'list'" + }, + "sections": { + "type": "array", + "required": false, + "default": ["To Do", "In Progress", "Done"], + "description": "Array of section names to create in order" + }, + "notes": { + "type": "string", + "required": false, + "description": "Project description" + }, + "color": { + "type": "string", + "required": false, + "description": "Project color (e.g., 'dark-green', 'dark-blue', 'dark-red', 'light-orange')" + } + }, + "steps": [ + { + "id": "buildProjectData", + "name": "Build project payload", + "type": "code", + "code": { + "source": "const { name, workspaceGid, teamGid, layout, notes, color } = $.input;\n\nif (!name || !workspaceGid) throw new Error('name and workspaceGid are required');\n\nconst data = {\n data: {\n name,\n workspace: workspaceGid,\n default_view: layout || 'board'\n }\n};\n\nif (teamGid) data.data.team = teamGid;\nif (notes) data.data.notes = notes;\nif (color) data.data.color = color;\n\nreturn data;" + } + }, + { + "id": "createProject", + "name": "Create the project", + "type": "action", + "action": { + "platform": "asana", + "actionId": "conn_mod_def::GJ0Bawb3mf4::pqp2OSmlQGeWkjw2bInUnw", + "connectionKey": "$.input.asanaConnectionKey", + "data": "$.steps.buildProjectData.output" + } + }, + { + "id": "prepareSections", + "name": "Prepare section list", + "type": "code", + "code": { + "source": "const sections = $.input.sections && $.input.sections.length > 0 ? $.input.sections : ['To Do', 'In Progress', 'Done'];\nconst projectGid = $.steps.createProject.response?.data?.gid;\nif (!projectGid) throw new Error('Project creation failed - no GID returned');\nreturn { sections: sections.map(name => ({ name })), projectGid };" + } + }, + { + "id": "createSections", + "name": "Create each section in the project", + "type": "loop", + "loop": { + "over": "$.steps.prepareSections.output.sections", + "as": "section", + "maxConcurrency": 1, + "steps": [ + { + "id": "createSection", + "name": "Create a section", + "type": "action", + "action": { + "platform": "asana", + "actionId": "conn_mod_def::GJ0BfEFfym8::raAyuUYGRHSpshZqnwy5vw", + "connectionKey": "$.input.asanaConnectionKey", + "pathVars": { + "projectGid": "$.steps.prepareSections.output.projectGid" + }, + "data": { + "data": { + "name": "$.loop.section.name" + } + } + } + } + ] + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const project = $.steps.createProject.response?.data || {};\nconst iterations = $.steps.createSections?.response?.iterations || [];\nconst createdSections = iterations.map(r => {\n const s = r?.createSection?.response?.data;\n return s ? { gid: s.gid, name: s.name } : null;\n}).filter(Boolean);\n\nreturn {\n projectGid: project.gid || '',\n name: project.name || '',\n layout: project.default_view || $.input.layout || 'board',\n permalink: project.permalink_url || '',\n sections: createdSections,\n sectionCount: createdSections.length,\n summary: `Created project \"${project.name || $.input.name}\" with ${createdSections.length} section${createdSections.length === 1 ? '' : 's'}: ${createdSections.map(s => s.name).join(', ')}`\n};" + } + } + ] +} diff --git a/flows/asana/asana-create-task.flow.json b/flows/asana/asana-create-task.flow.json new file mode 100644 index 0000000..ce26264 --- /dev/null +++ b/flows/asana/asana-create-task.flow.json @@ -0,0 +1,112 @@ +{ + "key": "asana-create-task", + "name": "Create Task in Asana", + "description": "Creates a task in Asana with optional project and section assignment. Handles the multi-step pattern: create task, then optionally add it to a specific section within the project.", + "version": "1", + "inputs": { + "asanaConnectionKey": { + "type": "string", + "required": true, + "description": "Asana connection key", + "connection": { "platform": "asana" } + }, + "name": { + "type": "string", + "required": true, + "description": "Task name" + }, + "workspaceGid": { + "type": "string", + "required": true, + "description": "Workspace GID where the task will be created" + }, + "projectGid": { + "type": "string", + "required": false, + "description": "Project GID to add the task to" + }, + "sectionGid": { + "type": "string", + "required": false, + "description": "Section GID to place the task in (requires projectGid)" + }, + "assignee": { + "type": "string", + "required": false, + "description": "Assignee GID or email address" + }, + "notes": { + "type": "string", + "required": false, + "description": "Task description (plain text)" + }, + "htmlNotes": { + "type": "string", + "required": false, + "description": "Task description (HTML). Overrides notes if both provided." + }, + "dueOn": { + "type": "string", + "required": false, + "description": "Due date in YYYY-MM-DD format" + }, + "tags": { + "type": "array", + "required": false, + "description": "Array of tag GIDs to apply" + }, + "priority": { + "type": "string", + "required": false, + "description": "Priority: 'high', 'medium', 'low', or custom field value" + } + }, + "steps": [ + { + "id": "buildTaskData", + "name": "Build task payload", + "type": "code", + "code": { + "source": "const { name, workspaceGid, projectGid, assignee, notes, htmlNotes, dueOn, tags } = $.input;\n\nif (!name || !workspaceGid) throw new Error('name and workspaceGid are required');\n\nconst data = {\n data: {\n name,\n workspace: workspaceGid\n }\n};\n\nif (projectGid) data.data.projects = [projectGid];\nif (assignee) data.data.assignee = assignee;\nif (htmlNotes) {\n data.data.html_notes = htmlNotes;\n} else if (notes) {\n data.data.notes = notes;\n}\nif (dueOn) data.data.due_on = dueOn;\nif (tags && tags.length > 0) data.data.tags = tags;\n\nreturn data;" + } + }, + { + "id": "createTask", + "name": "Create the task via Asana API", + "type": "action", + "action": { + "platform": "asana", + "actionId": "conn_mod_def::GJ0BjZE573g::hRws6xOKRGC8Xxv8Q09cLQ", + "connectionKey": "$.input.asanaConnectionKey", + "data": "$.steps.buildTaskData.output" + } + }, + { + "id": "addToSection", + "name": "Move task to specified section", + "type": "action", + "if": "$.input.sectionGid", + "action": { + "platform": "asana", + "actionId": "conn_mod_def::GJ0Be7uRrrE::WDFMGLjJT1ae4k1iHhqsZg", + "connectionKey": "$.input.asanaConnectionKey", + "pathVars": { + "sectionGid": "$.input.sectionGid" + }, + "data": { + "data": { + "task": "$.steps.createTask.response.data.gid" + } + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const task = $.steps.createTask.response?.data || {};\nconst sectionResult = $.steps.addToSection?.response || null;\n\nreturn {\n taskGid: task.gid || '',\n name: task.name || '',\n assignee: task.assignee ? (task.assignee.name || task.assignee.gid) : 'unassigned',\n dueOn: task.due_on || null,\n projectGid: $.input.projectGid || null,\n sectionGid: $.input.sectionGid || null,\n movedToSection: !!sectionResult,\n permalink: task.permalink_url || '',\n summary: `Created task \"${task.name || $.input.name}\"${task.assignee ? ' assigned to ' + (task.assignee.name || task.assignee.gid) : ''}${task.due_on ? ' due ' + task.due_on : ''}`\n};" + } + } + ] +} diff --git a/flows/asana/asana-list-project-tasks.flow.json b/flows/asana/asana-list-project-tasks.flow.json new file mode 100644 index 0000000..dda8217 --- /dev/null +++ b/flows/asana/asana-list-project-tasks.flow.json @@ -0,0 +1,62 @@ +{ + "key": "asana-list-project-tasks", + "name": "List Project Tasks in Asana", + "description": "Lists all tasks in an Asana project with full details. Handles the two-step pattern: list task references from the project, then fetch each task's full details including assignee, due date, and completion status.", + "version": "1", + "inputs": { + "asanaConnectionKey": { + "type": "string", + "required": true, + "description": "Asana connection key", + "connection": { "platform": "asana" } + }, + "projectGid": { + "type": "string", + "required": true, + "description": "Project GID to list tasks from" + }, + "completedSince": { + "type": "string", + "required": false, + "description": "Only return tasks completed after this date (ISO 8601). Use 'now' to exclude completed tasks." + }, + "limit": { + "type": "number", + "required": false, + "default": 50, + "description": "Max number of tasks to return (1-100)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { completedSince, limit } = $.input;\nconst params = {};\nif (completedSince) params.completed_since = completedSince;\nparams.limit = String(Math.max(1, Math.min(100, limit || 50)));\nparams.opt_fields = 'gid,name,assignee,assignee.name,due_on,completed,completed_at,notes,tags,tags.name,permalink_url,memberships.section.name';\nreturn { params };" + } + }, + { + "id": "listTasks", + "name": "List tasks in the project", + "type": "action", + "action": { + "platform": "asana", + "actionId": "conn_mod_def::GJ0Bk2jM_T0::FjnidkW2RUaBsB5eoIK9Sw", + "connectionKey": "$.input.asanaConnectionKey", + "pathVars": { + "projectGid": "$.input.projectGid" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Format the task list", + "type": "code", + "code": { + "source": "const resp = $.steps.listTasks.response || {};\nconst tasks = (resp.data || []).map(t => ({\n gid: t.gid,\n name: t.name,\n assignee: t.assignee ? (t.assignee.name || t.assignee.gid) : 'unassigned',\n dueOn: t.due_on || null,\n completed: !!t.completed,\n completedAt: t.completed_at || null,\n section: t.memberships && t.memberships[0] && t.memberships[0].section ? t.memberships[0].section.name : null,\n tags: (t.tags || []).map(tag => tag.name || tag.gid),\n permalink: t.permalink_url || '',\n notes: t.notes ? t.notes.substring(0, 200) : ''\n}));\n\nconst open = tasks.filter(t => !t.completed).length;\nconst done = tasks.filter(t => t.completed).length;\n\nreturn {\n tasks,\n totalCount: tasks.length,\n openCount: open,\n completedCount: done,\n projectGid: $.input.projectGid,\n hasMore: !!resp.next_page,\n summary: `Found ${tasks.length} task${tasks.length === 1 ? '' : 's'} (${open} open, ${done} completed)`\n};" + } + } + ] +} diff --git a/flows/asana/asana-search-tasks.flow.json b/flows/asana/asana-search-tasks.flow.json new file mode 100644 index 0000000..92944b1 --- /dev/null +++ b/flows/asana/asana-search-tasks.flow.json @@ -0,0 +1,98 @@ +{ + "key": "asana-search-tasks", + "name": "Search Tasks in Asana", + "description": "Searches for tasks in an Asana workspace using filters like assignee, project, completion status, due dates, and text. Returns enriched results with assignee names, sections, and tags.", + "version": "1", + "inputs": { + "asanaConnectionKey": { + "type": "string", + "required": true, + "description": "Asana connection key", + "connection": { "platform": "asana" } + }, + "workspaceGid": { + "type": "string", + "required": true, + "description": "Workspace GID to search within" + }, + "text": { + "type": "string", + "required": false, + "description": "Text to search for in task names and descriptions" + }, + "assignee": { + "type": "string", + "required": false, + "description": "Assignee GID or 'me' to filter by assignment" + }, + "projectGid": { + "type": "string", + "required": false, + "description": "Project GID to search within" + }, + "completed": { + "type": "boolean", + "required": false, + "description": "Filter by completion status. Omit to return both." + }, + "dueOnBefore": { + "type": "string", + "required": false, + "description": "Return tasks due on or before this date (YYYY-MM-DD)" + }, + "dueOnAfter": { + "type": "string", + "required": false, + "description": "Return tasks due on or after this date (YYYY-MM-DD)" + }, + "isBlocked": { + "type": "boolean", + "required": false, + "description": "Filter for tasks that are blocked by dependencies" + }, + "sortBy": { + "type": "string", + "required": false, + "default": "modified_at", + "description": "Sort field: 'due_date', 'created_at', 'completed_at', 'likes', or 'modified_at'" + }, + "sortAscending": { + "type": "boolean", + "required": false, + "default": false, + "description": "Sort ascending (true) or descending (false)" + } + }, + "steps": [ + { + "id": "buildSearchParams", + "name": "Build search query parameters", + "type": "code", + "code": { + "source": "const { text, assignee, projectGid, completed, dueOnBefore, dueOnAfter, isBlocked, sortBy, sortAscending } = $.input;\n\nconst params = {\n opt_fields: 'gid,name,assignee,assignee.name,due_on,completed,completed_at,notes,tags,tags.name,permalink_url,memberships.section.name'\n};\n\nif (text) params.text = text;\nif (assignee) params['assignee.any'] = assignee;\nif (projectGid) params['projects.any'] = projectGid;\nif (completed === true) params.completed = 'true';\nif (completed === false) params.completed = 'false';\nif (dueOnBefore) params['due_on.before'] = dueOnBefore;\nif (dueOnAfter) params['due_on.after'] = dueOnAfter;\nif (isBlocked === true) params.is_blocked = 'true';\nif (isBlocked === false) params.is_blocked = 'false';\n\nparams.sort_by = sortBy || 'modified_at';\nparams.sort_ascending = String(sortAscending === true);\n\nreturn { params };" + } + }, + { + "id": "searchTasks", + "name": "Search tasks in the workspace", + "type": "action", + "action": { + "platform": "asana", + "actionId": "conn_mod_def::GJ0BmJsc69Q::2k2XfYayTCahfyAcdAYtYA", + "connectionKey": "$.input.asanaConnectionKey", + "pathVars": { + "workspaceGid": "$.input.workspaceGid" + }, + "queryParams": "$.steps.buildSearchParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Format search results", + "type": "code", + "code": { + "source": "const resp = $.steps.searchTasks.response || {};\nconst tasks = (resp.data || []).map(t => ({\n gid: t.gid,\n name: t.name,\n assignee: t.assignee ? (t.assignee.name || t.assignee.gid) : 'unassigned',\n dueOn: t.due_on || null,\n completed: !!t.completed,\n completedAt: t.completed_at || null,\n section: t.memberships && t.memberships[0] && t.memberships[0].section ? t.memberships[0].section.name : null,\n tags: (t.tags || []).map(tag => tag.name || tag.gid),\n permalink: t.permalink_url || '',\n notes: t.notes ? t.notes.substring(0, 200) : ''\n}));\n\nconst open = tasks.filter(t => !t.completed).length;\nconst done = tasks.filter(t => t.completed).length;\nconst filters = [];\nif ($.input.text) filters.push('text: \"' + $.input.text + '\"');\nif ($.input.assignee) filters.push('assignee: ' + $.input.assignee);\nif ($.input.projectGid) filters.push('project: ' + $.input.projectGid);\nif ($.input.completed !== undefined) filters.push($.input.completed ? 'completed' : 'incomplete');\n\nreturn {\n tasks,\n totalCount: tasks.length,\n openCount: open,\n completedCount: done,\n filters: filters.join(', ') || 'none',\n hasMore: !!resp.next_page,\n summary: `Found ${tasks.length} task${tasks.length === 1 ? '' : 's'} (${open} open, ${done} completed)${filters.length ? ' matching: ' + filters.join(', ') : ''}`\n};" + } + } + ] +} diff --git a/flows/attio/README.md b/flows/attio/README.md new file mode 100644 index 0000000..dd0d2b8 --- /dev/null +++ b/flows/attio/README.md @@ -0,0 +1,233 @@ +--- +name: attio +description: | + Attio CRM integration flows for the One CLI. Ready-to-run workflows for + managing people, companies, notes, tasks, and lists in Attio -- handling + upsert matching, filter/sort queries, and Attio's values-as-arrays data model. +triggers: + - "attio" + - "/attio" + - "upsert person" + - "upsert company" + - "add to crm" + - "crm search" + - "search contacts" + - "create task" + - "add note" + - "add to list" +--- + +# Attio Flows + +Ready-to-run workflows for [Attio CRM](https://attio.com) via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add attio # Connect your Attio workspace +one --agent list # Find your connection key +``` + +## Discovery + +Some flows require record IDs or list slugs. Find them with: + +```bash +# Search for a person or company record (to get record IDs for notes/tasks/lists) +one flow execute attio-search-records.flow.json \ + --input attioConnectionKey="" \ + --input object="people" \ + --input limit=10 + +# List available lists (to get list slugs for add-to-list) +one --agent actions search attio "list lists" +one --agent actions execute attio +``` + +## Flows + +### Upsert Person + +Creates or updates a person record by matching on email. If a person with the +given email exists, the record is updated; otherwise a new one is created. + +```bash +one flow execute attio-upsert-person.flow.json \ + --input attioConnectionKey="" \ + --input email="ada@example.com" \ + --input firstName="Ada" \ + --input lastName="Lovelace" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `attioConnectionKey` | Yes | Your Attio connection key | +| `email` | Yes | Email address (matching attribute) | +| `firstName` | No | First name | +| `lastName` | No | Last name | +| `phone` | No | Phone number | +| `description` | No | Bio or description | + +**What it does under the hood:** + +1. Builds a `data.values` payload with email, name, phone, and description attributes +2. Calls Attio's PUT `/objects/people/records?matching_attribute=email_addresses` (assert/upsert endpoint) +3. Returns the record ID, web URL, and creation timestamp + +### Upsert Company + +Creates or updates a company record by matching on domain. + +```bash +one flow execute attio-upsert-company.flow.json \ + --input attioConnectionKey="" \ + --input domain="acme.com" \ + --input name="Acme Corp" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `attioConnectionKey` | Yes | Your Attio connection key | +| `domain` | Yes | Company domain (matching attribute) | +| `name` | No | Company name | +| `description` | No | Company description | + +**What it does under the hood:** + +1. Builds a `data.values` payload with domain, name, and description attributes +2. Calls Attio's PUT `/objects/companies/records?matching_attribute=domains` (assert/upsert endpoint) +3. Returns the record ID, web URL, and creation timestamp + +### Search Records + +Queries people, companies, or any custom object with optional filtering and sorting. + +```bash +one flow execute attio-search-records.flow.json \ + --input attioConnectionKey="" \ + --input object="people" \ + --input limit=20 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `attioConnectionKey` | Yes | Your Attio connection key | +| `object` | Yes | Object slug (`people`, `companies`, or custom slug) | +| `filter` | No | Attio filter object (default: `{}` for all records) | +| `sorts` | No | Array of sort objects, e.g., `[{"direction":"asc","attribute":"name"}]` | +| `limit` | No | Max records to return (1-500, default 50) | +| `offset` | No | Pagination offset (default 0) | + +**What it does under the hood:** + +1. Builds a query body with filter, sorts, limit, and offset +2. Calls Attio's POST `/objects/{object}/records/query` +3. Extracts readable values (names, emails, domains) from Attio's values-as-arrays format +4. Returns a flat array of records with common fields extracted + +### Add Note + +Creates a note attached to any record (person, company, etc.). + +```bash +one flow execute attio-add-note.flow.json \ + --input attioConnectionKey="" \ + --input parentObject="people" \ + --input parentRecordId="891dcbfc-9141-415d-9b2a-2238a6cc012d" \ + --input title="Call Notes" \ + --input content="Discussed pricing. Follow up next week." +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `attioConnectionKey` | Yes | Your Attio connection key | +| `parentObject` | Yes | Object slug (`people`, `companies`, etc.) | +| `parentRecordId` | Yes | UUID of the record to attach the note to | +| `title` | Yes | Note title (plaintext) | +| `content` | Yes | Note body | +| `format` | No | `plaintext` (default) or `markdown` | + +**What it does under the hood:** + +1. Sends the note payload to Attio's POST `/notes` endpoint +2. Supports both plaintext and markdown formatting +3. Returns the note ID and creation timestamp + +### Create Task + +Creates a task, optionally linked to a record and assigned to a team member. + +```bash +one flow execute attio-create-task.flow.json \ + --input attioConnectionKey="" \ + --input content="Follow up with Ada about pricing proposal" \ + --input deadlineAt="2025-06-01T00:00:00.000Z" \ + --input assigneeEmail="teammate@company.com" \ + --input linkedObject="people" \ + --input linkedRecordId="891dcbfc-9141-415d-9b2a-2238a6cc012d" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `attioConnectionKey` | Yes | Your Attio connection key | +| `content` | Yes | Task description (plaintext, max 2000 chars) | +| `deadlineAt` | No | ISO 8601 deadline (e.g., `2025-06-01T00:00:00.000Z`) | +| `assigneeEmail` | No | Workspace member email to assign | +| `linkedObject` | No | Object slug of record to link (`people`, `companies`) | +| `linkedRecordId` | No | UUID of record to link | + +**What it does under the hood:** + +1. Builds a task payload with content, deadline, assignees, and linked records +2. Calls Attio's POST `/tasks` endpoint +3. Returns the task ID and creation timestamp + +### Add to List + +Adds a record to an Attio list (sales pipeline, onboarding, etc.). + +```bash +one flow execute attio-add-to-list.flow.json \ + --input attioConnectionKey="" \ + --input list="enterprise_sales" \ + --input parentObject="companies" \ + --input parentRecordId="bf071e1f-6035-429d-b874-d83ea64ea13b" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `attioConnectionKey` | Yes | Your Attio connection key | +| `list` | Yes | List slug or UUID | +| `parentObject` | Yes | Object slug (`people`, `companies`, etc.) | +| `parentRecordId` | Yes | UUID of the record to add | +| `entryValues` | No | Optional attribute values for the list entry | + +**What it does under the hood:** + +1. Sends the entry payload to Attio's POST `/lists/{list}/entries` +2. Returns the entry ID, list ID, and creation timestamp + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Enrich a person**: Chain `attio-search-records` (to find a record) with `attio-add-note` (to log context). +- **Sales pipeline**: Use `attio-upsert-company` then `attio-add-to-list` to add deals to a pipeline. +- **Meeting follow-up**: After a meeting, use `attio-create-task` to create follow-up tasks linked to the contact. +- **Sync from email**: Chain a Gmail read flow into `attio-upsert-person` to auto-create CRM records from inbound email. +- **Custom objects**: All record flows work with custom objects -- just pass the object slug instead of `people` or `companies`. + +The orchestration knowledge is in the flow's `code` steps. Read them to understand Attio's values-as-arrays data model, upsert matching, and filter syntax -- then build your own variations. diff --git a/flows/attio/attio-add-note.flow.json b/flows/attio/attio-add-note.flow.json new file mode 100644 index 0000000..f4f9683 --- /dev/null +++ b/flows/attio/attio-add-note.flow.json @@ -0,0 +1,69 @@ +{ + "key": "attio-add-note", + "name": "Add a Note to an Attio Record", + "description": "Creates a note attached to a person, company, or any object record in Attio. Supports plaintext or markdown formatting.", + "version": "1", + "inputs": { + "attioConnectionKey": { + "type": "string", + "required": true, + "description": "Attio connection key", + "connection": { "platform": "attio" } + }, + "parentObject": { + "type": "string", + "required": true, + "description": "Object slug the record belongs to (e.g., 'people', 'companies')" + }, + "parentRecordId": { + "type": "string", + "required": true, + "description": "UUID of the record to attach the note to" + }, + "title": { + "type": "string", + "required": true, + "description": "Note title (plaintext only)" + }, + "content": { + "type": "string", + "required": true, + "description": "Note content body" + }, + "format": { + "type": "string", + "required": false, + "default": "plaintext", + "description": "Content format: 'plaintext' or 'markdown'" + } + }, + "steps": [ + { + "id": "createNote", + "name": "Create the note via Attio API", + "type": "action", + "action": { + "platform": "attio", + "actionId": "conn_mod_def::GJ0CZWRtEuU::n-NZ0VSqT3ym9zT9fBrefQ", + "connectionKey": "$.input.attioConnectionKey", + "data": { + "data": { + "parent_object": "$.input.parentObject", + "parent_record_id": "$.input.parentRecordId", + "title": "$.input.title", + "format": "$.input.format", + "content": "$.input.content" + } + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createNote.response || {};\nconst data = resp.data || {};\nconst id = data.id || {};\n\nreturn {\n noteId: id.note_id || '',\n parentObject: data.parent_object || $.input.parentObject,\n parentRecordId: data.parent_record_id || $.input.parentRecordId,\n title: data.title || $.input.title,\n createdAt: data.created_at || '',\n summary: id.note_id\n ? `Created note \"${$.input.title}\" on ${$.input.parentObject} record ${$.input.parentRecordId}`\n : 'Failed to create note'\n};" + } + } + ] +} diff --git a/flows/attio/attio-add-to-list.flow.json b/flows/attio/attio-add-to-list.flow.json new file mode 100644 index 0000000..4720cc5 --- /dev/null +++ b/flows/attio/attio-add-to-list.flow.json @@ -0,0 +1,65 @@ +{ + "key": "attio-add-to-list", + "name": "Add a Record to an Attio List", + "description": "Adds a person, company, or other record to an Attio list. Useful for managing sales pipelines, onboarding lists, or any workflow list.", + "version": "1", + "inputs": { + "attioConnectionKey": { + "type": "string", + "required": true, + "description": "Attio connection key", + "connection": { "platform": "attio" } + }, + "list": { + "type": "string", + "required": true, + "description": "List slug or UUID (e.g., 'enterprise_sales')" + }, + "parentObject": { + "type": "string", + "required": true, + "description": "Object slug the record belongs to (e.g., 'people', 'companies')" + }, + "parentRecordId": { + "type": "string", + "required": true, + "description": "UUID of the record to add to the list" + }, + "entryValues": { + "type": "object", + "required": false, + "default": {}, + "description": "Optional attribute values for the list entry. Map of attribute slug to array of values." + } + }, + "steps": [ + { + "id": "addEntry", + "name": "Create list entry via Attio API", + "type": "action", + "action": { + "platform": "attio", + "actionId": "conn_mod_def::GJ0CWgKBjg0::vDH4_yvrQymWm020xEcQyg", + "connectionKey": "$.input.attioConnectionKey", + "pathVars": { + "list": "$.input.list" + }, + "data": { + "data": { + "parent_record_id": "$.input.parentRecordId", + "parent_object": "$.input.parentObject", + "entry_values": "$.input.entryValues" + } + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.addEntry.response || {};\nconst data = resp.data || {};\nconst id = data.id || {};\n\nreturn {\n entryId: id.entry_id || '',\n listId: id.list_id || '',\n parentRecordId: data.parent_record_id || $.input.parentRecordId,\n parentObject: data.parent_object || $.input.parentObject,\n createdAt: data.created_at || '',\n summary: id.entry_id\n ? `Added ${$.input.parentObject} record ${$.input.parentRecordId} to list ${$.input.list}`\n : 'Failed to add record to list'\n};" + } + } + ] +} diff --git a/flows/attio/attio-create-task.flow.json b/flows/attio/attio-create-task.flow.json new file mode 100644 index 0000000..a63f392 --- /dev/null +++ b/flows/attio/attio-create-task.flow.json @@ -0,0 +1,68 @@ +{ + "key": "attio-create-task", + "name": "Create a Task in Attio", + "description": "Creates a task in Attio CRM, optionally linked to a person or company record and assigned to a workspace member.", + "version": "1", + "inputs": { + "attioConnectionKey": { + "type": "string", + "required": true, + "description": "Attio connection key", + "connection": { "platform": "attio" } + }, + "content": { + "type": "string", + "required": true, + "description": "Task description (plaintext, max 2000 chars)" + }, + "deadlineAt": { + "type": "string", + "required": false, + "description": "Deadline in ISO 8601 format (e.g., '2025-06-01T00:00:00.000Z'), or null" + }, + "assigneeEmail": { + "type": "string", + "required": false, + "description": "Email of the workspace member to assign the task to" + }, + "linkedObject": { + "type": "string", + "required": false, + "description": "Object slug of linked record (e.g., 'people', 'companies')" + }, + "linkedRecordId": { + "type": "string", + "required": false, + "description": "UUID of the record to link to the task" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build the task payload", + "type": "code", + "code": { + "source": "const { content, deadlineAt, assigneeEmail, linkedObject, linkedRecordId } = $.input;\n\nif (!content) throw new Error('content is required');\n\nconst data = {\n content: content,\n format: 'plaintext',\n deadline_at: deadlineAt || null,\n is_completed: false,\n linked_records: [],\n assignees: []\n};\n\nif (linkedObject && linkedRecordId) {\n data.linked_records.push({\n target_object: linkedObject,\n target_record_id: linkedRecordId\n });\n}\n\nif (assigneeEmail) {\n data.assignees.push({\n workspace_member_email_address: assigneeEmail\n });\n}\n\nreturn { data };" + } + }, + { + "id": "createTask", + "name": "Create the task via Attio API", + "type": "action", + "action": { + "platform": "attio", + "actionId": "conn_mod_def::GJ0CchbSRbc::m93snD2gRyC3NxGNhTSPEA", + "connectionKey": "$.input.attioConnectionKey", + "data": "$.steps.buildPayload.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createTask.response || {};\nconst data = resp.data || {};\nconst id = data.id || {};\n\nreturn {\n taskId: id.task_id || '',\n content: data.content_plaintext || $.input.content,\n deadlineAt: data.deadline_at || null,\n isCompleted: data.is_completed || false,\n createdAt: data.created_at || '',\n summary: id.task_id\n ? `Created task \"${$.input.content.substring(0, 60)}\" (${id.task_id})`\n : 'Failed to create task'\n};" + } + } + ] +} diff --git a/flows/attio/attio-search-records.flow.json b/flows/attio/attio-search-records.flow.json new file mode 100644 index 0000000..e5f343d --- /dev/null +++ b/flows/attio/attio-search-records.flow.json @@ -0,0 +1,74 @@ +{ + "key": "attio-search-records", + "name": "Search Records in Attio", + "description": "Queries people, companies, or any custom object in Attio with optional filtering and sorting. Returns records with all attribute values.", + "version": "1", + "inputs": { + "attioConnectionKey": { + "type": "string", + "required": true, + "description": "Attio connection key", + "connection": { "platform": "attio" } + }, + "object": { + "type": "string", + "required": true, + "description": "Object slug or ID to search (e.g., 'people', 'companies', or a custom object slug)" + }, + "filter": { + "type": "object", + "required": false, + "default": {}, + "description": "Attio filter object. See Attio filtering guide. Pass {} for no filter." + }, + "sorts": { + "type": "array", + "required": false, + "description": "Array of sort objects. Each: { direction: 'asc'|'desc', attribute: 'slug' }." + }, + "limit": { + "type": "number", + "required": false, + "default": 50, + "description": "Max records to return (default 50, max 500)" + }, + "offset": { + "type": "number", + "required": false, + "default": 0, + "description": "Number of records to skip for pagination" + } + }, + "steps": [ + { + "id": "buildBody", + "name": "Build query body with filter, sorts, and pagination", + "type": "code", + "code": { + "source": "const { filter, sorts, limit, offset } = $.input;\n\nconst body = {\n filter: filter || {},\n limit: Math.max(1, Math.min(500, limit || 50)),\n offset: offset || 0\n};\n\nif (sorts && Array.isArray(sorts) && sorts.length > 0) {\n body.sorts = sorts;\n}\n\nreturn body;" + } + }, + { + "id": "queryRecords", + "name": "Query records via Attio API", + "type": "action", + "action": { + "platform": "attio", + "actionId": "conn_mod_def::GJ0CbN-LPiU::A6h6M9QkS0Cs2Lt9CdPH7Q", + "connectionKey": "$.input.attioConnectionKey", + "pathVars": { + "object": "$.input.object" + }, + "data": "$.steps.buildBody.output" + } + }, + { + "id": "formatResult", + "name": "Format the response with extracted records", + "type": "code", + "code": { + "source": "const resp = $.steps.queryRecords.response || {};\nconst records = resp.data || [];\n\nconst formatted = records.map(r => {\n const id = r.id || {};\n const result = {\n recordId: id.record_id || '',\n webUrl: r.web_url || '',\n createdAt: r.created_at || ''\n };\n\n // Extract common readable values\n const vals = r.values || {};\n for (const [key, arr] of Object.entries(vals)) {\n if (!Array.isArray(arr) || arr.length === 0) continue;\n const v = arr[0];\n if (v.full_name) result[key] = v.full_name;\n else if (v.email_address) result[key] = v.email_address;\n else if (v.domain) result[key] = v.domain;\n else if (v.value !== undefined) result[key] = v.value;\n else if (v.first_name || v.last_name) result[key] = [v.first_name, v.last_name].filter(Boolean).join(' ');\n }\n\n return result;\n});\n\nreturn {\n records: formatted,\n totalReturned: formatted.length,\n object: $.input.object,\n summary: `Found ${formatted.length} ${$.input.object} record${formatted.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/attio/attio-upsert-company.flow.json b/flows/attio/attio-upsert-company.flow.json new file mode 100644 index 0000000..b8f2202 --- /dev/null +++ b/flows/attio/attio-upsert-company.flow.json @@ -0,0 +1,68 @@ +{ + "key": "attio-upsert-company", + "name": "Upsert a Company in Attio", + "description": "Creates or updates a company record in Attio CRM by matching on domain. If a company with the given domain exists, their record is updated; otherwise a new company is created.", + "version": "1", + "inputs": { + "attioConnectionKey": { + "type": "string", + "required": true, + "description": "Attio connection key", + "connection": { "platform": "attio" } + }, + "domain": { + "type": "string", + "required": true, + "description": "Company domain (e.g., 'acme.com') used as the matching attribute" + }, + "name": { + "type": "string", + "required": false, + "description": "Company name" + }, + "description": { + "type": "string", + "required": false, + "description": "Description of the company" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build the values payload for the company record", + "type": "code", + "code": { + "source": "const { domain, name, description } = $.input;\n\nif (!domain) throw new Error('domain is required');\n\nconst values = {\n domains: [{ domain: domain, root_domain: domain }]\n};\n\nif (name) {\n values.name = [{ value: name }];\n}\n\nif (description) {\n values.description = [{ value: description }];\n}\n\nreturn { values };" + } + }, + { + "id": "upsertCompany", + "name": "Assert (upsert) the company record via Attio API", + "type": "action", + "action": { + "platform": "attio", + "actionId": "conn_mod_def::GJ0CaZyhaFQ::Kzy79dY6TWW-84Z-BA-awA", + "connectionKey": "$.input.attioConnectionKey", + "pathVars": { + "object": "companies" + }, + "queryParams": { + "matching_attribute": "domains" + }, + "data": { + "data": { + "values": "$.steps.buildPayload.output.values" + } + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.upsertCompany.response || {};\nconst data = resp.data || {};\nconst id = data.id || {};\n\nreturn {\n recordId: id.record_id || '',\n objectId: id.object_id || '',\n webUrl: data.web_url || '',\n createdAt: data.created_at || '',\n domain: $.input.domain,\n summary: id.record_id\n ? `Upserted company ${$.input.domain} (${id.record_id})`\n : 'Failed to upsert company'\n};" + } + } + ] +} diff --git a/flows/attio/attio-upsert-person.flow.json b/flows/attio/attio-upsert-person.flow.json new file mode 100644 index 0000000..ac6ca8f --- /dev/null +++ b/flows/attio/attio-upsert-person.flow.json @@ -0,0 +1,78 @@ +{ + "key": "attio-upsert-person", + "name": "Upsert a Person in Attio", + "description": "Creates or updates a person record in Attio CRM by matching on email address. If a person with the given email exists, their record is updated; otherwise a new person is created.", + "version": "1", + "inputs": { + "attioConnectionKey": { + "type": "string", + "required": true, + "description": "Attio connection key", + "connection": { "platform": "attio" } + }, + "email": { + "type": "string", + "required": true, + "description": "Email address of the person (used as the matching attribute)" + }, + "firstName": { + "type": "string", + "required": false, + "description": "First name" + }, + "lastName": { + "type": "string", + "required": false, + "description": "Last name" + }, + "phone": { + "type": "string", + "required": false, + "description": "Phone number" + }, + "description": { + "type": "string", + "required": false, + "description": "Description or bio" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build the values payload for the person record", + "type": "code", + "code": { + "source": "const { email, firstName, lastName, phone, description } = $.input;\n\nif (!email) throw new Error('email is required');\n\nconst values = {\n email_addresses: [{ email_address: email }]\n};\n\nif (firstName || lastName) {\n const name = {};\n if (firstName) name.first_name = firstName;\n if (lastName) name.last_name = lastName;\n values.name = [name];\n}\n\nif (phone) {\n values.phone_numbers = [{ original_phone_number: phone }];\n}\n\nif (description) {\n values.description = [{ value: description }];\n}\n\nreturn { values };" + } + }, + { + "id": "upsertPerson", + "name": "Assert (upsert) the person record via Attio API", + "type": "action", + "action": { + "platform": "attio", + "actionId": "conn_mod_def::GJ0CaZyhaFQ::Kzy79dY6TWW-84Z-BA-awA", + "connectionKey": "$.input.attioConnectionKey", + "pathVars": { + "object": "people" + }, + "queryParams": { + "matching_attribute": "email_addresses" + }, + "data": { + "data": { + "values": "$.steps.buildPayload.output.values" + } + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.upsertPerson.response || {};\nconst data = resp.data || {};\nconst id = data.id || {};\n\nreturn {\n recordId: id.record_id || '',\n objectId: id.object_id || '',\n webUrl: data.web_url || '',\n createdAt: data.created_at || '',\n email: $.input.email,\n summary: id.record_id\n ? `Upserted person ${$.input.email} (${id.record_id})`\n : 'Failed to upsert person'\n};" + } + } + ] +} diff --git a/flows/cal/README.md b/flows/cal/README.md new file mode 100644 index 0000000..4a3155e --- /dev/null +++ b/flows/cal/README.md @@ -0,0 +1,119 @@ +--- +name: cal +description: | + Cal.com integration flows for the One CLI. Create bookings, list events, + check availability, and manage event types on Cal.com. +triggers: + - "cal.com" + - "cal" + - "create booking" + - "list bookings" + - "event types cal" + - "availability" + - "/cal" +--- + +# Cal.com Flows + +Ready-to-run workflows for Cal.com via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add cal-com # Connect your Cal.com account +one --agent list # Find your connection key +``` + +## Discovery + +Creating a booking requires an `eventTypeId`. Find yours using the manage event types flow: + +```bash +# List all your event types (returns IDs, names, and durations) +one flow execute cal-manage-event-types.flow.json \ + --input calConnectionKey="" \ + --input operation="list-event-types" +``` + +## Flows + +### Create Booking + +Book an event on Cal.com for an attendee. + +```bash +one flow execute cal-create-booking.flow.json \ + --input calConnectionKey="" \ + --input eventTypeId=12345 \ + --input start="2024-03-15T10:00:00Z" \ + --input attendeeName="Alice Smith" \ + --input attendeeEmail="alice@example.com" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `calConnectionKey` | Yes | Cal.com connection key | +| `eventTypeId` | Yes | Event type ID to book | +| `start` | Yes | Start time (ISO 8601) | +| `attendeeName` | Yes | Attendee's name | +| `attendeeEmail` | Yes | Attendee's email | +| `attendeeTimeZone` | No | Time zone (default: America/New_York) | +| `notes` | No | Booking notes | +| `metadata` | No | Custom metadata object | + +### List Bookings + +List bookings with optional status and date filtering. + +```bash +one flow execute cal-list-bookings.flow.json \ + --input calConnectionKey="" \ + --input status="upcoming" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `calConnectionKey` | Yes | Cal.com connection key | +| `status` | No | 'upcoming', 'past', 'cancelled', 'unconfirmed' | +| `afterStart` | No | Filter after this date (ISO 8601) | +| `beforeEnd` | No | Filter before this date (ISO 8601) | + +### Manage Event Types + +List event types or check available time slots. + +```bash +# List event types +one flow execute cal-manage-event-types.flow.json \ + --input calConnectionKey="" \ + --input operation="list-event-types" + +# Check availability +one flow execute cal-manage-event-types.flow.json \ + --input calConnectionKey="" \ + --input operation="get-slots" \ + --input eventTypeId="12345" \ + --input startTime="2024-03-15T00:00:00Z" \ + --input endTime="2024-03-22T00:00:00Z" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `calConnectionKey` | Yes | Cal.com connection key | +| `operation` | Yes | 'list-event-types' or 'get-slots' | +| `eventTypeId` | For get-slots | Event type ID | +| `startTime` | For get-slots | Availability window start (ISO 8601) | +| `endTime` | For get-slots | Availability window end (ISO 8601) | + +## Adapting These Flows + +- **Scheduling assistant**: Chain `cal-manage-event-types` (get-slots) into `cal-create-booking` to find and book the next available slot. +- **Booking digest**: Use `cal-list-bookings` and pipe results into a Slack or email notification. +- **Cancel/reschedule**: Use the cancel or reschedule actions with a booking UID from `cal-list-bookings`. diff --git a/flows/cal/cal-create-booking.flow.json b/flows/cal/cal-create-booking.flow.json new file mode 100644 index 0000000..8c2dd80 --- /dev/null +++ b/flows/cal/cal-create-booking.flow.json @@ -0,0 +1,79 @@ +{ + "key": "cal-create-booking", + "name": "Create a Booking on Cal.com", + "description": "Create a new booking on Cal.com by specifying an event type, attendee info, start time, and optional metadata.", + "version": "1", + "inputs": { + "calConnectionKey": { + "type": "string", + "required": true, + "description": "Cal.com connection key", + "connection": { "platform": "cal-com" } + }, + "eventTypeId": { + "type": "number", + "required": true, + "description": "Event type ID to book" + }, + "start": { + "type": "string", + "required": true, + "description": "Start time in ISO 8601 format (e.g., '2024-03-15T10:00:00Z')" + }, + "attendeeName": { + "type": "string", + "required": true, + "description": "Attendee's full name" + }, + "attendeeEmail": { + "type": "string", + "required": true, + "description": "Attendee's email address" + }, + "attendeeTimeZone": { + "type": "string", + "required": false, + "default": "America/New_York", + "description": "Attendee's time zone (e.g., 'America/New_York')" + }, + "notes": { + "type": "string", + "required": false, + "description": "Booking notes or message from the attendee" + }, + "metadata": { + "type": "object", + "required": false, + "description": "Custom metadata key-value pairs" + } + }, + "steps": [ + { + "id": "buildBooking", + "name": "Build booking payload", + "type": "code", + "code": { + "source": "const { eventTypeId, start, attendeeName, attendeeEmail, attendeeTimeZone, notes, metadata } = $.input;\nif (!eventTypeId || !start || !attendeeName || !attendeeEmail) throw new Error('eventTypeId, start, attendeeName, and attendeeEmail are required');\nconst body = {\n eventTypeId,\n start,\n attendee: {\n name: attendeeName,\n email: attendeeEmail,\n timeZone: attendeeTimeZone || 'America/New_York'\n }\n};\nif (notes) body.notes = notes;\nif (metadata) body.metadata = metadata;\nreturn body;" + } + }, + { + "id": "createBooking", + "name": "Create booking via Cal.com API", + "type": "action", + "action": { + "platform": "cal-com", + "actionId": "conn_mod_def::GJ1AAj_wmBI::YGhi0tsiQrup-PPSYLzi_Q", + "connectionKey": "$.input.calConnectionKey", + "data": "$.steps.buildBooking.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createBooking.response || {};\nconst booking = resp.data || resp;\nreturn {\n bookingId: booking.id || booking.uid || '',\n uid: booking.uid || '',\n title: booking.title || '',\n startTime: booking.startTime || $.input.start,\n endTime: booking.endTime || '',\n status: booking.status || '',\n created: !!(booking.id || booking.uid),\n summary: (booking.id || booking.uid)\n ? `Booked \"${booking.title || 'event'}\" for ${$.input.attendeeName}`\n : 'Failed to create booking'\n};" + } + } + ] +} diff --git a/flows/cal/cal-list-bookings.flow.json b/flows/cal/cal-list-bookings.flow.json new file mode 100644 index 0000000..94e0772 --- /dev/null +++ b/flows/cal/cal-list-bookings.flow.json @@ -0,0 +1,58 @@ +{ + "key": "cal-list-bookings", + "name": "List Bookings from Cal.com", + "description": "List and filter bookings from Cal.com. Supports filtering by status and date range.", + "version": "1", + "inputs": { + "calConnectionKey": { + "type": "string", + "required": true, + "description": "Cal.com connection key", + "connection": { "platform": "cal-com" } + }, + "status": { + "type": "string", + "required": false, + "description": "Filter by status: 'upcoming', 'past', 'cancelled', 'unconfirmed'" + }, + "afterStart": { + "type": "string", + "required": false, + "description": "Filter bookings after this ISO 8601 date" + }, + "beforeEnd": { + "type": "string", + "required": false, + "description": "Filter bookings before this ISO 8601 date" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { status, afterStart, beforeEnd } = $.input;\nconst params = {};\nif (status) params.status = status;\nif (afterStart) params['afterStart'] = afterStart;\nif (beforeEnd) params['beforeEnd'] = beforeEnd;\nreturn { params };" + } + }, + { + "id": "listBookings", + "name": "List bookings via Cal.com API", + "type": "action", + "action": { + "platform": "cal-com", + "actionId": "conn_mod_def::GJ1ABacT6P4::TQSYRwLQT02RtPGc25_8Qg", + "connectionKey": "$.input.calConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.listBookings.response || {};\nconst bookings = resp.data?.bookings || resp.bookings || resp.data || [];\nconst list = Array.isArray(bookings) ? bookings : [];\nreturn {\n bookings: list.map(b => ({\n id: b.id,\n uid: b.uid,\n title: b.title,\n startTime: b.startTime,\n endTime: b.endTime,\n status: b.status,\n attendees: (b.attendees || []).map(a => ({ name: a.name, email: a.email }))\n })),\n count: list.length,\n summary: `Found ${list.length} booking${list.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/cal/cal-manage-event-types.flow.json b/flows/cal/cal-manage-event-types.flow.json new file mode 100644 index 0000000..f37460f --- /dev/null +++ b/flows/cal/cal-manage-event-types.flow.json @@ -0,0 +1,79 @@ +{ + "key": "cal-manage-event-types", + "name": "List Event Types and Check Availability on Cal.com", + "description": "List your Cal.com event types or check available slots for a specific event type.", + "version": "1", + "inputs": { + "calConnectionKey": { + "type": "string", + "required": true, + "description": "Cal.com connection key", + "connection": { "platform": "cal-com" } + }, + "operation": { + "type": "string", + "required": true, + "description": "Operation: 'list-event-types' or 'get-slots'" + }, + "eventTypeId": { + "type": "string", + "required": false, + "description": "Event type ID (required for get-slots)" + }, + "startTime": { + "type": "string", + "required": false, + "description": "Start of availability window in ISO 8601 (required for get-slots)" + }, + "endTime": { + "type": "string", + "required": false, + "description": "End of availability window in ISO 8601 (required for get-slots)" + } + }, + "steps": [ + { + "id": "route", + "name": "Route to the correct operation", + "type": "code", + "code": { + "source": "const op = ($.input.operation || '').toLowerCase();\nif (op !== 'list-event-types' && op !== 'get-slots') throw new Error('operation must be \"list-event-types\" or \"get-slots\"');\nif (op === 'get-slots' && (!$.input.eventTypeId || !$.input.startTime || !$.input.endTime)) throw new Error('get-slots requires eventTypeId, startTime, and endTime');\nreturn { op };" + } + }, + { + "id": "listEventTypes", + "name": "List event types", + "type": "action", + "if": "$.steps.route.output.op === 'list-event-types'", + "action": { + "platform": "cal-com", + "actionId": "conn_mod_def::GJ1AIttlHew::eRhyfhySRPqVF2J0s_KUqg", + "connectionKey": "$.input.calConnectionKey" + } + }, + { + "id": "getSlots", + "name": "Get available slots for an event type", + "type": "action", + "if": "$.steps.route.output.op === 'get-slots'", + "action": { + "platform": "cal-com", + "actionId": "conn_mod_def::GJ1AhZMPqcg::nDkv6m-rTM2fmZcn7X9ZbA", + "connectionKey": "$.input.calConnectionKey", + "queryParams": { + "eventTypeId": "$.input.eventTypeId", + "startTime": "$.input.startTime", + "endTime": "$.input.endTime" + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const op = $.steps.route.output.op;\nif (op === 'list-event-types') {\n const resp = $.steps.listEventTypes?.response || {};\n const types = resp.data?.eventTypes || resp.event_types || resp.data || [];\n const list = Array.isArray(types) ? types : [];\n return {\n eventTypes: list.map(t => ({ id: t.id, title: t.title, slug: t.slug, length: t.length, description: t.description })),\n count: list.length,\n summary: `Found ${list.length} event type${list.length === 1 ? '' : 's'}`\n };\n} else {\n const resp = $.steps.getSlots?.response || {};\n const slots = resp.data?.slots || resp.slots || resp.data || {};\n const allSlots = Object.values(slots).flat();\n return {\n slots,\n totalSlots: allSlots.length,\n summary: `Found ${allSlots.length} available slot${allSlots.length === 1 ? '' : 's'}`\n };\n}" + } + } + ] +} diff --git a/flows/calendly/README.md b/flows/calendly/README.md new file mode 100644 index 0000000..eebf02d --- /dev/null +++ b/flows/calendly/README.md @@ -0,0 +1,95 @@ +--- +name: calendly +description: | + Calendly integration flows for the One CLI. List scheduled events, retrieve + invitee details, and manage event types. Handles the user URI lookup pattern + that Calendly requires. +triggers: + - "calendly" + - "list events calendly" + - "invitees" + - "event types calendly" + - "scheduled events" + - "/calendly" +--- + +# Calendly Flows + +Ready-to-run workflows for Calendly via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add calendly # Connect your Calendly account +one --agent list # Find your connection key +``` + +## Flows + +### List Scheduled Events + +List your scheduled Calendly events with optional date range and status filtering. Automatically resolves the current user URI. + +```bash +one flow execute calendly-list-events.flow.json \ + --input calendlyConnectionKey="" \ + --input status="active" \ + --input minStartTime="2024-03-01T00:00:00Z" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `calendlyConnectionKey` | Yes | Calendly connection key | +| `status` | No | 'active' or 'canceled' | +| `minStartTime` | No | Minimum start time (ISO 8601) | +| `maxStartTime` | No | Maximum start time (ISO 8601) | +| `count` | No | Number of events (default 20) | + +**What it does under the hood:** + +1. Fetches current user via `GET /users/me` to get the user URI +2. Passes user URI as a required filter parameter +3. Lists events via `GET /scheduled_events` with filters + +### Get Event Invitees + +Retrieve invitees for a specific scheduled event. + +```bash +one flow execute calendly-get-event-invitees.flow.json \ + --input calendlyConnectionKey="" \ + --input eventUuid="abc123-def456" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `calendlyConnectionKey` | Yes | Calendly connection key | +| `eventUuid` | Yes | UUID of the scheduled event | +| `status` | No | Filter by 'active' or 'canceled' | + +### List Event Types + +List all your Calendly event types with scheduling links and configuration. + +```bash +one flow execute calendly-list-event-types.flow.json \ + --input calendlyConnectionKey="" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `calendlyConnectionKey` | Yes | Calendly connection key | +| `active` | No | Filter by active status (true/false) | + +## Adapting These Flows + +- **Meeting prep**: Chain `calendly-list-events` into `calendly-get-event-invitees` to get attendee details for upcoming meetings. +- **CRM sync**: Pipe invitee data into HubSpot or ActiveCampaign contact creation flows. +- **Daily digest**: Filter events by today's date and send a summary to Slack or email. diff --git a/flows/calendly/calendly-get-event-invitees.flow.json b/flows/calendly/calendly-get-event-invitees.flow.json new file mode 100644 index 0000000..56770b4 --- /dev/null +++ b/flows/calendly/calendly-get-event-invitees.flow.json @@ -0,0 +1,56 @@ +{ + "key": "calendly-get-event-invitees", + "name": "Get Invitees for a Calendly Event", + "description": "Retrieve the list of invitees for a specific Calendly scheduled event, including their names, emails, and response status.", + "version": "1", + "inputs": { + "calendlyConnectionKey": { + "type": "string", + "required": true, + "description": "Calendly connection key", + "connection": { "platform": "calendly" } + }, + "eventUuid": { + "type": "string", + "required": true, + "description": "UUID of the scheduled event (from the event URI)" + }, + "status": { + "type": "string", + "required": false, + "description": "Filter invitees by status: 'active' or 'canceled'" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const params = {};\nif ($.input.status) params.status = $.input.status;\nreturn { params };" + } + }, + { + "id": "listInvitees", + "name": "List invitees for the event", + "type": "action", + "action": { + "platform": "calendly", + "actionId": "conn_mod_def::GJ1Ct14BaDw::_7gdhEnERJe0AltyvEanUA", + "connectionKey": "$.input.calendlyConnectionKey", + "pathVars": { + "uuid": "$.input.eventUuid" + }, + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.listInvitees.response || {};\nconst invitees = resp.collection || [];\nreturn {\n invitees: invitees.map(i => ({\n uri: i.uri,\n name: i.name,\n email: i.email,\n status: i.status,\n createdAt: i.created_at,\n timezone: i.timezone,\n questionsAndAnswers: i.questions_and_answers || []\n })),\n count: invitees.length,\n eventUuid: $.input.eventUuid,\n summary: `Found ${invitees.length} invitee${invitees.length === 1 ? '' : 's'} for event ${$.input.eventUuid}`\n};" + } + } + ] +} diff --git a/flows/calendly/calendly-list-event-types.flow.json b/flows/calendly/calendly-list-event-types.flow.json new file mode 100644 index 0000000..b753109 --- /dev/null +++ b/flows/calendly/calendly-list-event-types.flow.json @@ -0,0 +1,58 @@ +{ + "key": "calendly-list-event-types", + "name": "List Calendly Event Types", + "description": "List all event types for the current Calendly user, including scheduling links, duration, and availability details.", + "version": "1", + "inputs": { + "calendlyConnectionKey": { + "type": "string", + "required": true, + "description": "Calendly connection key", + "connection": { "platform": "calendly" } + }, + "active": { + "type": "boolean", + "required": false, + "description": "Filter by active status (true = only active, false = only inactive)" + } + }, + "steps": [ + { + "id": "getCurrentUser", + "name": "Get current Calendly user URI", + "type": "action", + "action": { + "platform": "calendly", + "actionId": "conn_mod_def::GJ1CuU4b8Vc::7IQskt_KTzKutFpBIR2Uwg", + "connectionKey": "$.input.calendlyConnectionKey" + } + }, + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const user = $.steps.getCurrentUser.response?.resource || $.steps.getCurrentUser.response || {};\nconst userUri = user.uri || '';\nif (!userUri) throw new Error('Could not determine current user URI');\nconst params = { user: userUri };\nif ($.input.active !== undefined) params.active = String($.input.active);\nreturn { params };" + } + }, + { + "id": "listEventTypes", + "name": "List event types", + "type": "action", + "action": { + "platform": "calendly", + "actionId": "conn_mod_def::GJ1CqXgQaDM::Axz-At86SF-Z9PH0cP-x7Q", + "connectionKey": "$.input.calendlyConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.listEventTypes.response || {};\nconst types = resp.collection || [];\nreturn {\n eventTypes: types.map(t => ({\n uri: t.uri,\n name: t.name,\n slug: t.slug,\n active: t.active,\n duration: t.duration,\n schedulingUrl: t.scheduling_url,\n type: t.type,\n color: t.color,\n description: t.description_plain || t.description_html || ''\n })),\n count: types.length,\n summary: `Found ${types.length} event type${types.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/calendly/calendly-list-events.flow.json b/flows/calendly/calendly-list-events.flow.json new file mode 100644 index 0000000..b30c46a --- /dev/null +++ b/flows/calendly/calendly-list-events.flow.json @@ -0,0 +1,74 @@ +{ + "key": "calendly-list-events", + "name": "List Scheduled Events from Calendly", + "description": "List scheduled events from Calendly with optional date range and status filtering. Fetches the current user URI automatically.", + "version": "1", + "inputs": { + "calendlyConnectionKey": { + "type": "string", + "required": true, + "description": "Calendly connection key", + "connection": { "platform": "calendly" } + }, + "status": { + "type": "string", + "required": false, + "description": "Filter by status: 'active' or 'canceled'" + }, + "minStartTime": { + "type": "string", + "required": false, + "description": "Minimum start time in ISO 8601 format" + }, + "maxStartTime": { + "type": "string", + "required": false, + "description": "Maximum start time in ISO 8601 format" + }, + "count": { + "type": "number", + "required": false, + "default": 20, + "description": "Number of events to return" + } + }, + "steps": [ + { + "id": "getCurrentUser", + "name": "Get current Calendly user URI", + "type": "action", + "action": { + "platform": "calendly", + "actionId": "conn_mod_def::GJ1CuU4b8Vc::7IQskt_KTzKutFpBIR2Uwg", + "connectionKey": "$.input.calendlyConnectionKey" + } + }, + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const user = $.steps.getCurrentUser.response?.resource || $.steps.getCurrentUser.response || {};\nconst userUri = user.uri || '';\nif (!userUri) throw new Error('Could not determine current user URI');\nconst params = { user: userUri, count: String($.input.count || 20) };\nif ($.input.status) params.status = $.input.status;\nif ($.input.minStartTime) params.min_start_time = $.input.minStartTime;\nif ($.input.maxStartTime) params.max_start_time = $.input.maxStartTime;\nreturn { params, userUri };" + } + }, + { + "id": "listEvents", + "name": "List scheduled events", + "type": "action", + "action": { + "platform": "calendly", + "actionId": "conn_mod_def::GJ1Ct_k8rnk::drsZp-rYRNutesYvaXyxMQ", + "connectionKey": "$.input.calendlyConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.listEvents.response || {};\nconst events = resp.collection || [];\nreturn {\n events: events.map(e => ({\n uri: e.uri,\n name: e.name,\n status: e.status,\n startTime: e.start_time,\n endTime: e.end_time,\n eventType: e.event_type,\n location: e.location || {},\n inviteesCount: e.invitees_counter?.total || 0\n })),\n count: events.length,\n pagination: resp.pagination || {},\n summary: `Found ${events.length} scheduled event${events.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/diffbot/README.md b/flows/diffbot/README.md new file mode 100644 index 0000000..0d24304 --- /dev/null +++ b/flows/diffbot/README.md @@ -0,0 +1,54 @@ +--- +name: diffbot +description: | + Diffbot article extraction flow for the One CLI. Extracts clean article text, + metadata, tags, and images from any URL using Diffbot's Article API. +triggers: + - "extract article" + - "diffbot" + - "article extraction" + - "/diffbot" +--- + +# Diffbot Flows + +Ready-to-run article extraction via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add diffbot # Connect your Diffbot account +one --agent list # Find your connection key +``` + +## Flows + +### Extract Article + +Extracts clean article text and structured metadata from any article, blog post, +or news page. Returns title, author, date, full text, tags, categories, images, +and sentiment score. + +```bash +one flow execute diffbot-extract-article.flow.json \ + --input diffbotConnectionKey="" \ + --input url="https://example.com/article" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `diffbotConnectionKey` | Yes | Your Diffbot connection key | +| `url` | Yes | URL of the article to extract | +| `discussion` | No | Set `false` to disable comment extraction | +| `maxTags` | No | Max tags to return (default: 10) | +| `fields` | No | Optional fields: links, extlinks, meta, querystring, breadcrumb, quotes | + +**What it does under the hood:** + +1. Builds query parameters with URL and optional extraction settings +2. Calls Diffbot Article API (`GET /v3/article`) +3. Extracts and formats title, author, date, text, tags, categories, images, and sentiment +4. Returns a clean, structured response ready for downstream use diff --git a/flows/diffbot/diffbot-extract-article.flow.json b/flows/diffbot/diffbot-extract-article.flow.json new file mode 100644 index 0000000..5743288 --- /dev/null +++ b/flows/diffbot/diffbot-extract-article.flow.json @@ -0,0 +1,65 @@ +{ + "key": "diffbot-extract-article", + "name": "Extract Article with Diffbot", + "description": "Extract clean article text, metadata, tags, and images from any URL using Diffbot's Article API. Handles multi-page concatenation, NLP analysis, and comment extraction automatically.", + "version": "1", + "inputs": { + "diffbotConnectionKey": { + "type": "string", + "required": true, + "description": "Diffbot connection key", + "connection": { "platform": "diffbot" } + }, + "url": { + "type": "string", + "required": true, + "description": "URL of the article/blog post to extract" + }, + "discussion": { + "type": "boolean", + "required": false, + "default": true, + "description": "Set false to disable comment extraction" + }, + "maxTags": { + "type": "number", + "required": false, + "default": 10, + "description": "Maximum number of auto-generated tags to return" + }, + "fields": { + "type": "string", + "required": false, + "description": "Optional fields to return: links, extlinks, meta, querystring, breadcrumb, quotes (comma-separated)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters for Diffbot API", + "type": "code", + "code": { + "source": "const { url, discussion, maxTags, fields } = $.input;\nif (!url) throw new Error('url is required');\n\nconst params = { url };\nif (discussion === false) params.discussion = 'false';\nif (maxTags && maxTags !== 10) params.maxTags = String(maxTags);\nif (fields) params.fields = fields;\n\nreturn { params };" + } + }, + { + "id": "extractArticle", + "name": "Call Diffbot Article API", + "type": "action", + "action": { + "platform": "diffbot", + "actionId": "conn_mod_def::GJ2To17xqMI::mITwa3lZQaCynW7ziCeEWw", + "connectionKey": "$.input.diffbotConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Extract and format article data", + "type": "code", + "code": { + "source": "const resp = $.steps.extractArticle.response || {};\nconst objects = resp.objects || [];\nconst article = objects[0] || {};\n\nreturn {\n title: article.title || resp.title || '',\n author: article.author || '',\n date: article.date || '',\n text: article.text || '',\n html: article.html || '',\n siteName: article.siteName || '',\n pageUrl: article.pageUrl || $.input.url,\n language: resp.humanLanguage || '',\n sentiment: article.sentiment || null,\n tags: (article.tags || []).map(t => ({ label: t.label, score: t.score, count: t.count })),\n categories: (article.categories || []).map(c => ({ name: c.name, score: c.score })),\n images: (article.images || []).map(i => ({ url: i.url, width: i.naturalWidth, height: i.naturalHeight, primary: i.primary || false })),\n numPages: article.numPages || 1,\n summary: article.title ? `Extracted \"${article.title}\" by ${article.author || 'unknown'} from ${article.siteName || 'unknown site'}` : 'No article found at URL'\n};" + } + } + ] +} diff --git a/flows/github/README.md b/flows/github/README.md new file mode 100644 index 0000000..910ea9c --- /dev/null +++ b/flows/github/README.md @@ -0,0 +1,325 @@ +--- +name: github +description: | + GitHub integration flows for the One CLI. Ready-to-run workflows that handle + the orchestration complexity (list+detail patterns, PR review aggregation, + search+inspect, webhook configuration) so you don't have to. +triggers: + - "github" + - "/github" + - "create issue" + - "list issues" + - "create pull request" + - "review pull requests" + - "search repos" + - "search repositories" + - "setup webhook" + - "commit history" +--- + +# GitHub Flows + +Ready-to-run workflows for GitHub via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add github # Connect your GitHub account +one --agent list # Find your connection key +``` + +## Flows + +### List Issues + +Lists issues from a repository with optional comment fetching. Handles the +GitHub API quirk where the issues endpoint also returns pull requests (filters +them out automatically). + +```bash +one flow execute github-list-issues.flow.json \ + --input githubConnectionKey="" \ + --input owner="withoneai" \ + --input repo="one" \ + --input state="open" \ + --input labels="bug" \ + --input maxResults=10 \ + --input includeComments=true +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `owner` | Yes | Repository owner (user or org) | +| `repo` | Yes | Repository name | +| `state` | No | Filter: `open`, `closed`, or `all` (default: `open`) | +| `labels` | No | Comma-separated label names to filter by | +| `assignee` | No | Filter by assignee. `*` for any, `none` for unassigned | +| `sort` | No | Sort by: `created`, `updated`, or `comments` | +| `maxResults` | No | Number of issues (1-100, default 10) | +| `includeComments` | No | Fetch comments per issue (default: false) | + +**What it does under the hood:** + +1. Builds query parameters from filters (state, labels, assignee, sort) +2. Lists issues via `GET /repos/{owner}/{repo}/issues` +3. Filters out pull requests (GitHub returns PRs in the issues endpoint) +4. Optionally fetches comments for each issue in parallel (5 concurrent) +5. Assembles clean response with labels, assignees, body preview, and comments + +--- + +### Create Issue + +Creates an issue with title, body, labels, assignees, and milestone. + +```bash +one flow execute github-create-issue.flow.json \ + --input githubConnectionKey="" \ + --input owner="withoneai" \ + --input repo="one" \ + --input title="Bug: login page 500 error" \ + --input body="Steps to reproduce:\n1. Go to /login\n2. Click submit with empty fields" \ + --input labels='["bug", "priority:high"]' \ + --input assignees='["moebot"]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `owner` | Yes | Repository owner | +| `repo` | Yes | Repository name | +| `title` | Yes | Issue title | +| `body` | No | Issue body (GitHub-flavored markdown) | +| `labels` | No | Label names to apply (array) | +| `assignees` | No | GitHub usernames to assign (array) | +| `milestone` | No | Milestone number | + +**What it does under the hood:** + +1. Validates required fields and builds the payload +2. Creates the issue via `POST /repos/{owner}/{repo}/issues` +3. Returns issue number, URL, applied labels, and assignees + +--- + +### Review Pull Requests + +Lists PRs with full details including diff stats, mergeable status, and review +summaries. Handles the multi-step orchestration: list PRs, fetch full details +(which includes additions/deletions/changed files), and fetch reviews. + +```bash +one flow execute github-pr-review.flow.json \ + --input githubConnectionKey="" \ + --input owner="withoneai" \ + --input repo="one" \ + --input state="open" \ + --input maxResults=5 \ + --input includeReviews=true +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `owner` | Yes | Repository owner | +| `repo` | Yes | Repository name | +| `state` | No | Filter: `open`, `closed`, or `all` (default: `open`) | +| `sort` | No | Sort by: `created`, `updated`, or `popularity` | +| `maxResults` | No | Number of PRs (1-30, default 5) | +| `includeReviews` | No | Fetch review status per PR (default: true) | + +**What it does under the hood:** + +1. Lists PRs via `GET /repos/{owner}/{repo}/pulls` +2. For each PR, fetches full details via `GET /repos/{owner}/{repo}/pulls/{number}` (this is the only way to get diff stats like additions/deletions) +3. Optionally fetches reviews via `GET /repos/{owner}/{repo}/pulls/{number}/reviews` +4. Computes review summary: approved count, changes-requested count, deduped by reviewer (only latest review per user counts) +5. Returns mergeable status, branch info, labels, and review breakdown + +--- + +### Create Pull Request + +Creates a PR with title, body, head/base branches, and draft mode. + +```bash +one flow execute github-create-pr.flow.json \ + --input githubConnectionKey="" \ + --input owner="withoneai" \ + --input repo="one" \ + --input title="feat: add webhook support" \ + --input body="## Summary\n- Adds webhook creation endpoint\n- Handles signature verification" \ + --input head="feature/webhooks" \ + --input base="main" \ + --input draft=true +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `owner` | Yes | Repository owner | +| `repo` | Yes | Repository name | +| `title` | Yes | PR title | +| `body` | No | PR description (GitHub-flavored markdown) | +| `head` | Yes | Branch with your changes (e.g., `feature-branch` or `fork-owner:branch`) | +| `base` | No | Target branch (default: `main`) | +| `draft` | No | Create as draft PR (default: false) | + +**What it does under the hood:** + +1. Validates required fields (title, head branch) +2. Creates the PR via `POST /repos/{owner}/{repo}/pulls` +3. Returns PR number, URL, branch info, and draft status + +--- + +### Search Repositories + +Searches GitHub repositories with optional recent commit fetching. Uses the +search+inspect pattern: find repos matching a query, then optionally fetch +recent commits for each. + +```bash +one flow execute github-repo-search.flow.json \ + --input githubConnectionKey="" \ + --input query="language:typescript stars:>1000 topic:cli" \ + --input sort="stars" \ + --input maxResults=10 \ + --input includeCommits=true +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `query` | Yes | GitHub search query (see syntax below) | +| `sort` | No | Sort: `stars`, `forks`, `help-wanted-issues`, `updated`, or `best-match` | +| `maxResults` | No | Number of repos (1-30, default 5) | +| `includeCommits` | No | Fetch 5 recent commits per repo (default: false) | + +**Search query examples:** + +| Query | Meaning | +|-------|---------| +| `language:typescript` | TypeScript repositories | +| `stars:>1000` | More than 1000 stars | +| `org:withoneai` | Repositories in an organization | +| `topic:cli` | Repos with the "cli" topic | +| `created:>2024-01-01` | Created after a date | +| `language:rust stars:>100 topic:wasm` | Combined filters | + +**What it does under the hood:** + +1. Builds search query with sort and pagination +2. Searches via `GET /search/repositories` +3. Optionally fetches recent commits for each repo in parallel (5 concurrent) +4. Returns stars, forks, language, topics, and optional commit history + +--- + +### Setup Webhook + +Creates a webhook on a repository with configurable events, content type, and +optional secret for signature verification. + +```bash +one flow execute github-webhook-setup.flow.json \ + --input githubConnectionKey="" \ + --input owner="withoneai" \ + --input repo="one" \ + --input url="https://your-server.com/webhook" \ + --input events='["push", "pull_request", "issues"]' \ + --input secret="your-webhook-secret" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `owner` | Yes | Repository owner | +| `repo` | Yes | Repository name | +| `url` | Yes | Webhook payload URL (must be HTTPS) | +| `events` | No | Events to trigger on (default: `["push"]`). Use `["*"]` for all. | +| `secret` | No | Secret for signature verification (recommended) | +| `contentType` | No | `json` or `form` (default: `json`) | +| `active` | No | Whether webhook is active immediately (default: true) | + +**Common event types:** + +| Event | Triggers on | +|-------|-------------| +| `push` | Any push to a branch | +| `pull_request` | PR opened, closed, merged, etc. | +| `issues` | Issue opened, closed, labeled, etc. | +| `issue_comment` | Comment on an issue or PR | +| `release` | Release published | +| `*` | All events | + +**What it does under the hood:** + +1. Validates URL is HTTPS +2. Builds webhook config with content type and optional secret +3. Creates webhook via `POST /repos/{owner}/{repo}/hooks` +4. Returns webhook ID, events, and test/ping URLs + +--- + +### Commit History + +Fetches commit history with filtering by branch, author, path, and date range. + +```bash +one flow execute github-commit-history.flow.json \ + --input githubConnectionKey="" \ + --input owner="withoneai" \ + --input repo="one" \ + --input branch="main" \ + --input author="moebot" \ + --input since="2024-01-01T00:00:00Z" \ + --input maxResults=20 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `githubConnectionKey` | Yes | Your GitHub connection key | +| `owner` | Yes | Repository owner | +| `repo` | Yes | Repository name | +| `branch` | No | Branch name (defaults to repo's default branch) | +| `author` | No | Filter by author (username or email) | +| `path` | No | Filter by file path (commits touching this path) | +| `since` | No | After this date (ISO 8601) | +| `until` | No | Before this date (ISO 8601) | +| `maxResults` | No | Number of commits (1-100, default 20) | + +**What it does under the hood:** + +1. Builds query parameters from filters (branch, author, path, date range) +2. Lists commits via `GET /repos/{owner}/{repo}/commits` +3. Parses commit messages, author info, verification status +4. Computes summary stats: unique authors, date range + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Issue triage bot**: Chain `github-list-issues` with a labeling action to auto-categorize new issues. +- **PR dashboard**: Use `github-pr-review` to build a daily digest of PRs needing review, pipe into Slack. +- **Release notes**: Combine `github-commit-history` (filter by date range between tags) with an LLM to generate changelogs. +- **Repo monitoring**: Use `github-repo-search` with `org:your-org` to track all repos, chain with `github-webhook-setup` to register webhooks. +- **Auto-assign**: Chain `github-list-issues` (unassigned) with an update action to assign based on labels. + +The orchestration knowledge is in each flow's `code` steps. Read them to understand the list+detail patterns, PR review deduplication, and search query construction -- then build your own variations. diff --git a/flows/github/github-commit-history.flow.json b/flows/github/github-commit-history.flow.json new file mode 100644 index 0000000..f89020b --- /dev/null +++ b/flows/github/github-commit-history.flow.json @@ -0,0 +1,88 @@ +{ + "key": "github-commit-history", + "name": "Get Commit History", + "description": "Fetch commit history for a repository with full details. Supports filtering by branch, author, path, and date range. Handles the list+detail pattern for commit data.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "owner": { + "type": "string", + "required": true, + "description": "Repository owner (user or org)" + }, + "repo": { + "type": "string", + "required": true, + "description": "Repository name" + }, + "branch": { + "type": "string", + "required": false, + "description": "Branch name (defaults to repo's default branch)" + }, + "author": { + "type": "string", + "required": false, + "description": "Filter by commit author (GitHub username or email)" + }, + "path": { + "type": "string", + "required": false, + "description": "Filter by file path (only commits touching this path)" + }, + "since": { + "type": "string", + "required": false, + "description": "Only commits after this date (ISO 8601 format, e.g., 2024-01-01T00:00:00Z)" + }, + "until": { + "type": "string", + "required": false, + "description": "Only commits before this date (ISO 8601 format)" + }, + "maxResults": { + "type": "number", + "required": false, + "default": 20, + "description": "Number of commits to return (1-100)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { branch, author, path, since, until, maxResults } = $.input;\nconst params = {\n per_page: String(Math.max(1, Math.min(100, maxResults || 20)))\n};\nif (branch) params.sha = branch;\nif (author) params.author = author;\nif (path) params.path = path;\nif (since) params.since = since;\nif (until) params.until = until;\nreturn { params };" + } + }, + { + "id": "listCommits", + "name": "List commits from the repository", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCmaFkUctCM::4e9h36E8QValq_8TFWnTOg", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Parse and format commit data", + "type": "code", + "code": { + "source": "const resp = $.steps.listCommits.response || [];\nconst commits = (Array.isArray(resp) ? resp : []).map(c => ({\n sha: c.sha,\n shortSha: c.sha?.substring(0, 7) || '',\n message: c.commit?.message || '',\n messageFirstLine: (c.commit?.message || '').split('\\n')[0],\n author: {\n name: c.commit?.author?.name || '',\n email: c.commit?.author?.email || '',\n login: c.author?.login || '',\n date: c.commit?.author?.date || ''\n },\n committer: {\n name: c.commit?.committer?.name || '',\n login: c.committer?.login || ''\n },\n stats: c.stats || null,\n url: c.html_url,\n verified: c.commit?.verification?.verified || false\n}));\n\n// Build summary stats\nconst authors = [...new Set(commits.map(c => c.author.login || c.author.name).filter(Boolean))];\nconst dateRange = commits.length > 0\n ? { from: commits[commits.length - 1].author.date, to: commits[0].author.date }\n : null;\n\nreturn {\n commits,\n totalFound: commits.length,\n repo: $.input.owner + '/' + $.input.repo,\n branch: $.input.branch || 'default',\n uniqueAuthors: authors,\n dateRange,\n filters: {\n author: $.input.author || 'any',\n path: $.input.path || 'any',\n since: $.input.since || 'any',\n until: $.input.until || 'any'\n },\n summary: 'Found ' + commits.length + ' commit' + (commits.length === 1 ? '' : 's') + ' in ' + $.input.owner + '/' + $.input.repo + ($.input.branch ? ' on ' + $.input.branch : '') + ' by ' + authors.length + ' author' + (authors.length === 1 ? '' : 's')\n};" + } + } + ] +} diff --git a/flows/github/github-create-issue.flow.json b/flows/github/github-create-issue.flow.json new file mode 100644 index 0000000..a01daba --- /dev/null +++ b/flows/github/github-create-issue.flow.json @@ -0,0 +1,82 @@ +{ + "key": "github-create-issue", + "name": "Create GitHub Issue", + "description": "Create an issue in a GitHub repository with title, body, labels, assignees, and milestone. Handles markdown formatting and label validation.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "owner": { + "type": "string", + "required": true, + "description": "Repository owner (user or org)" + }, + "repo": { + "type": "string", + "required": true, + "description": "Repository name" + }, + "title": { + "type": "string", + "required": true, + "description": "Issue title" + }, + "body": { + "type": "string", + "required": false, + "description": "Issue body (supports GitHub-flavored markdown)" + }, + "labels": { + "type": "array", + "required": false, + "description": "Label names to apply (e.g., ['bug', 'priority:high'])" + }, + "assignees": { + "type": "array", + "required": false, + "description": "GitHub usernames to assign" + }, + "milestone": { + "type": "number", + "required": false, + "description": "Milestone number to associate with" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build issue creation payload", + "type": "code", + "code": { + "source": "const { title, body, labels, assignees, milestone } = $.input;\nif (!title) throw new Error('title is required');\n\nconst payload = { title };\nif (body) payload.body = body;\nif (labels && labels.length > 0) payload.labels = labels;\nif (assignees && assignees.length > 0) payload.assignees = assignees;\nif (milestone) payload.milestone = milestone;\n\nreturn { payload };" + } + }, + { + "id": "createIssue", + "name": "Create the issue via GitHub API", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCXXCKZmYuk::Q6Ob6mmnRoavWZmGB2-K9g", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo" + }, + "data": "$.steps.buildPayload.output.payload" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createIssue.response || {};\nreturn {\n issueNumber: resp.number,\n title: resp.title,\n state: resp.state,\n url: resp.html_url,\n labels: (resp.labels || []).map(l => l.name || l),\n assignees: (resp.assignees || []).map(a => a.login),\n createdAt: resp.created_at,\n created: !!resp.number,\n summary: resp.number\n ? 'Created issue #' + resp.number + ': ' + resp.title + ' in ' + $.input.owner + '/' + $.input.repo\n : 'Failed to create issue'\n};" + } + } + ] +} diff --git a/flows/github/github-create-pr.flow.json b/flows/github/github-create-pr.flow.json new file mode 100644 index 0000000..24edd6f --- /dev/null +++ b/flows/github/github-create-pr.flow.json @@ -0,0 +1,84 @@ +{ + "key": "github-create-pr", + "name": "Create Pull Request", + "description": "Create a pull request in a GitHub repository. Supports title, body, base/head branches, draft mode, labels, assignees, and reviewers.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "owner": { + "type": "string", + "required": true, + "description": "Repository owner (user or org)" + }, + "repo": { + "type": "string", + "required": true, + "description": "Repository name" + }, + "title": { + "type": "string", + "required": true, + "description": "Pull request title" + }, + "body": { + "type": "string", + "required": false, + "description": "Pull request description (supports GitHub-flavored markdown)" + }, + "head": { + "type": "string", + "required": true, + "description": "The branch containing your changes (e.g., 'feature-branch' or 'fork-owner:branch')" + }, + "base": { + "type": "string", + "required": false, + "default": "main", + "description": "The branch you want to merge into (default: main)" + }, + "draft": { + "type": "boolean", + "required": false, + "default": false, + "description": "Create as a draft pull request" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build PR creation payload", + "type": "code", + "code": { + "source": "const { title, body, head, base, draft } = $.input;\nif (!title) throw new Error('title is required');\nif (!head) throw new Error('head branch is required');\n\nconst payload = {\n title,\n head,\n base: base || 'main'\n};\nif (body) payload.body = body;\nif (draft) payload.draft = true;\n\nreturn { payload };" + } + }, + { + "id": "createPR", + "name": "Create the pull request via GitHub API", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCQEQSHasPY::GMPh353yRuaB5HTGalx57g", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo" + }, + "data": "$.steps.buildPayload.output.payload" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createPR.response || {};\nreturn {\n prNumber: resp.number,\n title: resp.title,\n state: resp.state,\n draft: resp.draft || false,\n head: resp.head?.ref || $.input.head,\n base: resp.base?.ref || $.input.base || 'main',\n url: resp.html_url,\n diffUrl: resp.diff_url,\n created: !!resp.number,\n summary: resp.number\n ? 'Created PR #' + resp.number + ': ' + resp.title + ' (' + (resp.head?.ref || $.input.head) + ' -> ' + (resp.base?.ref || $.input.base) + ')'\n : 'Failed to create pull request'\n};" + } + } + ] +} diff --git a/flows/github/github-list-issues.flow.json b/flows/github/github-list-issues.flow.json new file mode 100644 index 0000000..8ad0de5 --- /dev/null +++ b/flows/github/github-list-issues.flow.json @@ -0,0 +1,137 @@ +{ + "key": "github-list-issues", + "name": "List and Read Repository Issues", + "description": "List issues from a GitHub repository with full details. Handles the list+detail pattern: first lists issues matching filters, then fetches each issue's full content including labels, assignees, and comments.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "owner": { + "type": "string", + "required": true, + "description": "Repository owner (user or org)" + }, + "repo": { + "type": "string", + "required": true, + "description": "Repository name" + }, + "state": { + "type": "string", + "required": false, + "default": "open", + "description": "Filter by state: open, closed, or all" + }, + "labels": { + "type": "string", + "required": false, + "description": "Comma-separated list of label names to filter by" + }, + "assignee": { + "type": "string", + "required": false, + "description": "Filter by assignee username. Use '*' for any, 'none' for unassigned." + }, + "sort": { + "type": "string", + "required": false, + "default": "created", + "description": "Sort by: created, updated, or comments" + }, + "maxResults": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of issues to return (1-100)" + }, + "includeComments": { + "type": "boolean", + "required": false, + "default": false, + "description": "Fetch comments for each issue (adds one API call per issue)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { state, labels, assignee, sort, maxResults } = $.input;\nconst params = {};\nparams.state = state || 'open';\nparams.sort = sort || 'created';\nparams.per_page = String(Math.max(1, Math.min(100, maxResults || 10)));\nif (labels) params.labels = labels;\nif (assignee) params.assignee = assignee;\nreturn { params };" + } + }, + { + "id": "listIssues", + "name": "List issues from the repository", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCXXDNfknD4::dqAYPXx9RHCqkVp3BiMZ3Q", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "filterIssues", + "name": "Filter out pull requests (GitHub returns PRs in issues endpoint)", + "type": "code", + "code": { + "source": "const resp = $.steps.listIssues.response || [];\nconst issues = (Array.isArray(resp) ? resp : []).filter(i => !i.pull_request);\nconst shouldFetchComments = $.input.includeComments && issues.length > 0;\nreturn { issues, count: shouldFetchComments ? issues.length : 0 };" + } + }, + { + "id": "fetchComments", + "name": "Fetch comments for each issue", + "type": "loop", + "if": "$.steps.filterIssues.output.count > 0", + "loop": { + "over": "$.steps.filterIssues.output.issues", + "as": "issue", + "maxConcurrency": 5, + "steps": [ + { + "id": "getComments", + "name": "Get comments for this issue", + "type": "action", + "onError": { "strategy": "continue" }, + "action": { + "platform": "github", + "actionId": "conn_mod_def::GDHR3UCjO-I::QMAOPBxLRNOHCWasVwHkog", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo", + "issue_number": "$.loop.issue.number" + }, + "queryParams": { "per_page": "10" } + } + }, + { + "id": "parseComments", + "name": "Parse comment data", + "type": "code", + "code": { + "source": "const comments = $.steps.getComments?.response || [];\nreturn {\n issueNumber: $.loop.issue.number,\n comments: (Array.isArray(comments) ? comments : []).map(c => ({\n id: c.id,\n author: c.user?.login || 'unknown',\n body: c.body?.substring(0, 500) || '',\n createdAt: c.created_at\n }))\n};" + } + } + ] + } + }, + { + "id": "formatResult", + "name": "Compile final result", + "type": "code", + "code": { + "source": "const issues = $.steps.filterIssues.output.issues || [];\nconst commentIterations = $.steps.fetchComments?.response?.iterations || [];\n\nconst commentMap = {};\ncommentIterations.forEach(iter => {\n const parsed = iter?.parseComments?.output;\n if (parsed) commentMap[parsed.issueNumber] = parsed.comments;\n});\n\nconst result = issues.map(i => {\n const item = {\n number: i.number,\n title: i.title,\n state: i.state,\n author: i.user?.login || 'unknown',\n assignees: (i.assignees || []).map(a => a.login),\n labels: (i.labels || []).map(l => typeof l === 'string' ? l : l.name),\n createdAt: i.created_at,\n updatedAt: i.updated_at,\n body: (i.body || '').substring(0, 500),\n commentsCount: i.comments || 0,\n url: i.html_url\n };\n if (commentMap[i.number]) item.comments = commentMap[i.number];\n return item;\n});\n\nreturn {\n issues: result,\n totalFound: result.length,\n repo: $.input.owner + '/' + $.input.repo,\n filters: { state: $.input.state || 'open', labels: $.input.labels || 'any', assignee: $.input.assignee || 'any' },\n summary: 'Found ' + result.length + ' issue' + (result.length === 1 ? '' : 's') + ' in ' + $.input.owner + '/' + $.input.repo\n};" + } + } + ] +} diff --git a/flows/github/github-pr-review.flow.json b/flows/github/github-pr-review.flow.json new file mode 100644 index 0000000..b4ffb67 --- /dev/null +++ b/flows/github/github-pr-review.flow.json @@ -0,0 +1,142 @@ +{ + "key": "github-pr-review", + "name": "Review Pull Requests", + "description": "List and review pull requests in a repository. Fetches PRs with full details including diff stats, reviews, and review comments. Handles the multi-step orchestration: list PRs, fetch details, fetch reviews.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "owner": { + "type": "string", + "required": true, + "description": "Repository owner (user or org)" + }, + "repo": { + "type": "string", + "required": true, + "description": "Repository name" + }, + "state": { + "type": "string", + "required": false, + "default": "open", + "description": "Filter by state: open, closed, or all" + }, + "sort": { + "type": "string", + "required": false, + "default": "created", + "description": "Sort by: created, updated, or popularity" + }, + "maxResults": { + "type": "number", + "required": false, + "default": 5, + "description": "Number of PRs to return (1-30)" + }, + "includeReviews": { + "type": "boolean", + "required": false, + "default": true, + "description": "Fetch review status for each PR" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { state, sort, maxResults } = $.input;\nconst params = {};\nparams.state = state || 'open';\nparams.sort = sort || 'created';\nparams.per_page = String(Math.max(1, Math.min(30, maxResults || 5)));\nreturn { params };" + } + }, + { + "id": "listPRs", + "name": "List pull requests from the repository", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCQEZpOKR3c::U8SIvgvaTbqv8fihVUwuQA", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "checkResults", + "name": "Check if any PRs were found", + "type": "code", + "code": { + "source": "const resp = $.steps.listPRs.response || [];\nconst prs = Array.isArray(resp) ? resp : [];\nreturn { prs, count: prs.length };" + } + }, + { + "id": "fetchDetails", + "name": "Fetch full details and reviews for each PR", + "type": "loop", + "if": "$.steps.checkResults.output.count > 0", + "loop": { + "over": "$.steps.checkResults.output.prs", + "as": "pr", + "maxConcurrency": 5, + "steps": [ + { + "id": "getPR", + "name": "Get full PR details (includes diff stats)", + "type": "action", + "onError": { "strategy": "continue" }, + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCQFO0Cix4Q::F7A7hKBzTbeyHnMR3lgHRg", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo", + "pull_number": "$.loop.pr.number" + } + } + }, + { + "id": "getReviews", + "name": "Get reviews for this PR", + "type": "action", + "onError": { "strategy": "continue" }, + "action": { + "platform": "github", + "actionId": "conn_mod_def::GDHSjBOz8es::rTL7yBWCSsy-Dr8zJAruyg", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo", + "pull_number": "$.loop.pr.number" + } + } + }, + { + "id": "parsePR", + "name": "Parse PR and review data", + "type": "code", + "code": { + "source": "const pr = $.steps.getPR?.response || $.loop.pr;\nconst reviews = $.steps.getReviews?.response || [];\n\nconst reviewList = (Array.isArray(reviews) ? reviews : []).map(r => ({\n user: r.user?.login || 'unknown',\n state: r.state,\n submittedAt: r.submitted_at\n}));\n\nconst latestByUser = {};\nreviewList.forEach(r => {\n if (!latestByUser[r.user] || new Date(r.submittedAt) > new Date(latestByUser[r.user].submittedAt)) {\n latestByUser[r.user] = r;\n }\n});\n\nconst approved = Object.values(latestByUser).filter(r => r.state === 'APPROVED').length;\nconst changesRequested = Object.values(latestByUser).filter(r => r.state === 'CHANGES_REQUESTED').length;\n\nreturn {\n number: pr.number,\n title: pr.title,\n state: pr.state,\n draft: pr.draft || false,\n author: pr.user?.login || 'unknown',\n head: pr.head?.ref || '',\n base: pr.base?.ref || '',\n additions: pr.additions || 0,\n deletions: pr.deletions || 0,\n changedFiles: pr.changed_files || 0,\n mergeable: pr.mergeable,\n mergeableState: pr.mergeable_state || '',\n labels: (pr.labels || []).map(l => l.name || l),\n assignees: (pr.assignees || []).map(a => a.login),\n reviewSummary: { approved, changesRequested, total: reviewList.length },\n reviews: reviewList,\n createdAt: pr.created_at,\n updatedAt: pr.updated_at,\n url: pr.html_url\n};" + } + } + ] + } + }, + { + "id": "formatResult", + "name": "Compile final result", + "type": "code", + "code": { + "source": "const iterations = $.steps.fetchDetails?.response?.iterations || [];\nconst prs = iterations.map(r => r?.parsePR?.output).filter(Boolean);\n\nreturn {\n pullRequests: prs,\n totalFound: prs.length,\n repo: $.input.owner + '/' + $.input.repo,\n filters: { state: $.input.state || 'open' },\n summary: 'Found ' + prs.length + ' pull request' + (prs.length === 1 ? '' : 's') + ' in ' + $.input.owner + '/' + $.input.repo\n};" + } + } + ] +} diff --git a/flows/github/github-repo-search.flow.json b/flows/github/github-repo-search.flow.json new file mode 100644 index 0000000..95b0df9 --- /dev/null +++ b/flows/github/github-repo-search.flow.json @@ -0,0 +1,111 @@ +{ + "key": "github-repo-search", + "name": "Search and Inspect Repositories", + "description": "Search GitHub repositories and fetch detailed information. Handles the search+detail pattern: finds repos matching a query, then fetches full metadata including languages, topics, and recent commits.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "query": { + "type": "string", + "required": true, + "description": "Search query using GitHub search syntax (e.g., 'language:typescript stars:>100', 'org:withoneai')" + }, + "sort": { + "type": "string", + "required": false, + "default": "best-match", + "description": "Sort by: stars, forks, help-wanted-issues, updated, or best-match" + }, + "maxResults": { + "type": "number", + "required": false, + "default": 5, + "description": "Number of repositories to return (1-30)" + }, + "includeCommits": { + "type": "boolean", + "required": false, + "default": false, + "description": "Fetch recent commits for each repo (adds one API call per repo)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build search parameters", + "type": "code", + "code": { + "source": "const { query, sort, maxResults } = $.input;\nif (!query) throw new Error('query is required');\nconst params = {\n q: query,\n per_page: String(Math.max(1, Math.min(30, maxResults || 5)))\n};\nif (sort && sort !== 'best-match') params.sort = sort;\nreturn { params };" + } + }, + { + "id": "searchRepos", + "name": "Search repositories", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GDHSkn2N6LI::TSvz_OHwQ_SqqG5oRQBIcw", + "connectionKey": "$.input.githubConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "checkResults", + "name": "Extract repositories from search response", + "type": "code", + "code": { + "source": "const resp = $.steps.searchRepos.response || {};\nconst items = resp.items || [];\nconst fetchCount = $.input.includeCommits ? items.length : 0;\nreturn { repos: items, count: fetchCount, totalCount: resp.total_count || 0, actualCount: items.length };" + } + }, + { + "id": "fetchDetails", + "name": "Fetch additional details for each repo", + "type": "loop", + "if": "$.steps.checkResults.output.count > 0", + "loop": { + "over": "$.steps.checkResults.output.repos", + "as": "repo", + "maxConcurrency": 5, + "steps": [ + { + "id": "getCommits", + "name": "Get recent commits", + "type": "action", + "onError": { "strategy": "continue" }, + "action": { + "platform": "github", + "actionId": "conn_mod_def::GCmaFkUctCM::4e9h36E8QValq_8TFWnTOg", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.loop.repo.owner.login", + "repo": "$.loop.repo.name" + }, + "queryParams": { "per_page": "5" } + } + }, + { + "id": "parseCommits", + "name": "Parse commit data", + "type": "code", + "code": { + "source": "const commits = $.steps.getCommits?.response || [];\nreturn {\n fullName: $.loop.repo.full_name,\n recentCommits: (Array.isArray(commits) ? commits : []).slice(0, 5).map(c => ({\n sha: c.sha?.substring(0, 7) || '',\n message: (c.commit?.message || '').split('\\n')[0].substring(0, 80),\n author: c.commit?.author?.name || c.author?.login || 'unknown',\n date: c.commit?.author?.date || ''\n }))\n};" + } + } + ] + } + }, + { + "id": "formatResult", + "name": "Compile final result", + "type": "code", + "code": { + "source": "const repos = $.steps.checkResults.output.repos || [];\nconst commitIterations = $.steps.fetchDetails?.response?.iterations || [];\n\nconst commitMap = {};\ncommitIterations.forEach(iter => {\n const parsed = iter?.parseCommits?.output;\n if (parsed) commitMap[parsed.fullName] = parsed.recentCommits;\n});\n\nconst result = repos.map(r => {\n const item = {\n fullName: r.full_name,\n description: (r.description || '').substring(0, 200),\n language: r.language,\n stars: r.stargazers_count || 0,\n forks: r.forks_count || 0,\n openIssues: r.open_issues_count || 0,\n topics: r.topics || [],\n isPrivate: r.private || false,\n defaultBranch: r.default_branch || 'main',\n updatedAt: r.updated_at,\n url: r.html_url\n };\n if (commitMap[r.full_name]) item.recentCommits = commitMap[r.full_name];\n return item;\n});\n\nreturn {\n repositories: result,\n totalFound: result.length,\n totalAvailable: $.steps.checkResults.output.totalCount,\n query: $.input.query,\n summary: 'Found ' + result.length + ' of ' + $.steps.checkResults.output.totalCount + ' repositories matching \"' + $.input.query + '\"'\n};" + } + } + ] +} diff --git a/flows/github/github-webhook-setup.flow.json b/flows/github/github-webhook-setup.flow.json new file mode 100644 index 0000000..7342720 --- /dev/null +++ b/flows/github/github-webhook-setup.flow.json @@ -0,0 +1,85 @@ +{ + "key": "github-webhook-setup", + "name": "Setup Repository Webhook", + "description": "Create a webhook on a GitHub repository. Configures the URL, events, content type, and optional secret for webhook signature verification.", + "version": "1", + "inputs": { + "githubConnectionKey": { + "type": "string", + "required": true, + "description": "GitHub connection key", + "connection": { "platform": "github" } + }, + "owner": { + "type": "string", + "required": true, + "description": "Repository owner (user or org)" + }, + "repo": { + "type": "string", + "required": true, + "description": "Repository name" + }, + "url": { + "type": "string", + "required": true, + "description": "Webhook payload URL (must be HTTPS)" + }, + "events": { + "type": "array", + "required": false, + "default": ["push"], + "description": "Events to trigger the webhook (e.g., ['push', 'pull_request', 'issues']). Use ['*'] for all events." + }, + "secret": { + "type": "string", + "required": false, + "description": "Webhook secret for signature verification (recommended)" + }, + "contentType": { + "type": "string", + "required": false, + "default": "json", + "description": "Content type: json or form" + }, + "active": { + "type": "boolean", + "required": false, + "default": true, + "description": "Whether the webhook is active immediately" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build webhook creation payload", + "type": "code", + "code": { + "source": "const { url, events, secret, contentType, active } = $.input;\nif (!url) throw new Error('url is required');\nif (!url.startsWith('https://')) throw new Error('Webhook URL must use HTTPS');\n\nconst config = {\n url,\n content_type: contentType || 'json',\n insecure_ssl: '0'\n};\nif (secret) config.secret = secret;\n\nconst payload = {\n name: 'web',\n active: active !== false,\n events: events && events.length > 0 ? events : ['push'],\n config\n};\n\nreturn { payload };" + } + }, + { + "id": "createWebhook", + "name": "Create the webhook via GitHub API", + "type": "action", + "action": { + "platform": "github", + "actionId": "conn_mod_def::GDHSvhKb8ko::DQOx2Fk7Sciape8SudQxAA", + "connectionKey": "$.input.githubConnectionKey", + "pathVars": { + "owner": "$.input.owner", + "repo": "$.input.repo" + }, + "data": "$.steps.buildPayload.output.payload" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createWebhook.response || {};\nreturn {\n webhookId: resp.id,\n url: resp.config?.url || $.input.url,\n events: resp.events || $.input.events || ['push'],\n active: resp.active,\n createdAt: resp.created_at,\n testUrl: resp.test_url,\n pingUrl: resp.ping_url,\n created: !!resp.id,\n summary: resp.id\n ? 'Created webhook #' + resp.id + ' on ' + $.input.owner + '/' + $.input.repo + ' for events: ' + (resp.events || []).join(', ')\n : 'Failed to create webhook'\n};" + } + } + ] +} diff --git a/flows/gmail/README.md b/flows/gmail/README.md new file mode 100644 index 0000000..73547ff --- /dev/null +++ b/flows/gmail/README.md @@ -0,0 +1,123 @@ +--- +name: gmail +description: | + Gmail integration flows for the One CLI. Ready-to-run workflows that handle + the orchestration complexity (MIME encoding, base64url, list+detail patterns) + so you don't have to. +triggers: + - "send email" + - "draft email" + - "create draft" + - "search emails" + - "get emails" + - "read emails" + - "list threads" + - "gmail" + - "/gmail" +--- + +# Gmail Flows + +Ready-to-run workflows for Gmail via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add gmail # Connect your Gmail account +one --agent list # Find your connection key +``` + +## Flows + +### Send Email + +Composes and sends an email. Handles MIME encoding, base64url conversion, HTML +formatting, non-ASCII subject encoding, CC/BCC, and reply threading. + +```bash +one flow execute gmail-send-email.flow.json \ + --input gmailConnectionKey="" \ + --input to="recipient@example.com" \ + --input subject="Hello from One" \ + --input body="Your message here.\n\nLine breaks work.\nSo do emojis 🚀" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `gmailConnectionKey` | Yes | Your Gmail connection key | +| `to` | Yes | Recipient(s), comma-separated | +| `subject` | Yes | Subject line (emoji and unicode safe) | +| `body` | Yes | Email body (plain text auto-converted to HTML) | +| `isHtml` | No | Set `true` if body is already HTML | +| `from` | No | Custom sender address | +| `cc` | No | CC recipients, comma-separated | +| `bcc` | No | BCC recipients, comma-separated | +| `replyTo` | No | Reply-To address | +| `threadId` | No | Thread ID for replies | +| `inReplyTo` | No | Message-ID being replied to | +| `references` | No | Message-ID chain for threading | +| `labelIds` | No | Labels to apply (default: `["INBOX", "UNREAD"]`) | + +**What it does under the hood:** + +1. Builds a MIME message with proper headers (From, To, Subject, Content-Type, etc.) +2. Encodes non-ASCII subjects using RFC 2047 (`=?UTF-8?B?...?=`) +3. Converts plain text to HTML (escapes special chars, `\n` to `
`) +4. Base64url-encodes the entire MIME message +5. Sends via Gmail API `POST /users/{userId}/messages/send` + +### Read Emails + +Searches and retrieves emails with full content. Handles the two-step +list+detail pattern that Gmail requires (list returns IDs only, each message +needs a separate fetch to get content). + +```bash +one flow execute gmail-read-emails.flow.json \ + --input gmailConnectionKey="" \ + --input query="from:alice subject:invoice" \ + --input maxResults=5 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `gmailConnectionKey` | Yes | Your Gmail connection key | +| `query` | No | Gmail search query (same syntax as the Gmail search bar) | +| `label` | No | Label filter (e.g., `INBOX`, `SENT`, `STARRED`) | +| `maxResults` | No | Number of emails (1-100, default 10) | +| `pageToken` | No | Pagination token for next page | + +**What it does under the hood:** + +1. Builds search query combining label and query filters +2. Lists message IDs via `GET /users/me/messages` with query params +3. Fetches each message in parallel (5 concurrent) via `GET /users/me/messages/{id}` with `format=full` +4. Decodes base64url-encoded bodies (prefers text/plain, falls back to text/html, handles nested multipart) +5. Extracts headers (From, To, Subject, Date) and assembles clean response + +**Search query examples:** + +| Query | Meaning | +|-------|---------| +| `from:alice@example.com` | From a specific sender | +| `subject:invoice` | Subject contains "invoice" | +| `is:unread label:INBOX` | Unread inbox messages | +| `has:attachment` | Has attachments | +| `after:2024/01/01` | After a date | +| `newer_than:7d` | Within the last 7 days | + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Reply to an email**: Use `gmail-send-email` with `threadId`, `inReplyTo`, and `references` from a previous read. +- **Search and forward**: Chain `gmail-read-emails` into `gmail-send-email` using a multi-step flow. +- **Auto-label**: Add a step after `gmail-read-emails` to call the modify-labels action. +- **Email digest**: Pipe `gmail-read-emails` output into a Slack or Notion action. + +The orchestration knowledge is in the flow's `code` steps. Read them to understand the MIME encoding, body decoding, and query construction patterns -- then build your own variations. diff --git a/flows/gmail/gmail-read-emails.flow.json b/flows/gmail/gmail-read-emails.flow.json new file mode 100644 index 0000000..b060b0f --- /dev/null +++ b/flows/gmail/gmail-read-emails.flow.json @@ -0,0 +1,110 @@ +{ + "key": "gmail-read-emails", + "name": "Read Emails from Gmail", + "description": "Search and read emails from Gmail with full content. Handles the two-step list+detail pattern: first lists message IDs matching a query, then fetches each message's full content with decoded headers and body.", + "version": "1", + "inputs": { + "gmailConnectionKey": { + "type": "string", + "required": true, + "description": "Gmail connection key", + "connection": { "platform": "gmail" } + }, + "query": { + "type": "string", + "required": false, + "description": "Gmail search query (same syntax as Gmail search bar). Examples: 'from:alice', 'subject:invoice after:2024/01/01', 'is:unread label:INBOX'" + }, + "label": { + "type": "string", + "required": false, + "description": "Gmail label to filter by (e.g., 'INBOX', 'SENT', 'STARRED')" + }, + "maxResults": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of emails to return (1-100)" + }, + "pageToken": { + "type": "string", + "required": false, + "description": "Pagination token from a previous response to get the next page" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build search query and params", + "type": "code", + "code": { + "source": "const { query, label, maxResults, pageToken } = $.input;\nconst max = Math.max(1, Math.min(100, maxResults || 10));\n\nlet q = '';\nif (label) q += 'label:' + label;\nif (query) q += (q ? ' ' : '') + query;\n\nconst params = { maxResults: String(max) };\nif (q) params.q = q;\nif (pageToken) params.pageToken = pageToken;\n\nreturn { params, q, max };" + } + }, + { + "id": "listMessages", + "name": "List message IDs matching the query", + "type": "action", + "action": { + "platform": "gmail", + "actionId": "conn_mod_def::GJ3odOE-fdw::ijLww5s-SCSplLQtLpxkrw", + "connectionKey": "$.input.gmailConnectionKey", + "pathVars": { "userId": "me" }, + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "checkResults", + "name": "Check if any messages were found", + "type": "code", + "code": { + "source": "const resp = $.steps.listMessages.response || {};\nconst messages = resp.messages || [];\nreturn { messageRefs: messages, count: messages.length, nextPageToken: resp.nextPageToken || null };" + } + }, + { + "id": "fetchMessages", + "name": "Fetch full content for each message", + "type": "loop", + "if": "$.steps.checkResults.output.count > 0", + "loop": { + "over": "$.steps.checkResults.output.messageRefs", + "as": "msg", + "maxConcurrency": 5, + "steps": [ + { + "id": "getMessage", + "name": "Get full message content", + "type": "action", + "onError": { "strategy": "continue" }, + "action": { + "platform": "gmail", + "actionId": "conn_mod_def::GJ3ocvMGOS8::D__3BgQSSzWtDUoOqLuX2A", + "connectionKey": "$.input.gmailConnectionKey", + "pathVars": { + "userId": "me", + "id": "$.loop.msg.id" + }, + "queryParams": { "format": "full" } + } + }, + { + "id": "parseMessage", + "name": "Extract headers and decode body", + "type": "code", + "code": { + "source": "const resp = $.steps.getMessage?.response;\nif (!resp || !resp.payload) return null;\n\nconst headers = resp.payload.headers || [];\nconst getH = (name) => (headers.find(h => h.name.toLowerCase() === name.toLowerCase()) || {}).value || '';\n\nfunction decodeBase64Url(data) {\n try {\n const fixed = data.replace(/-/g, '+').replace(/_/g, '/');\n const padded = fixed + '='.repeat((4 - fixed.length % 4) % 4);\n return atob(padded);\n } catch { return ''; }\n}\n\nfunction extractBody(payload) {\n if (payload.body && payload.body.data) return decodeBase64Url(payload.body.data);\n if (payload.parts) {\n for (const p of payload.parts) {\n if (p.mimeType === 'text/plain' && p.body && p.body.data) return decodeBase64Url(p.body.data);\n }\n for (const p of payload.parts) {\n if (p.mimeType === 'text/html' && p.body && p.body.data) return decodeBase64Url(p.body.data);\n }\n for (const p of payload.parts) {\n if (p.parts) {\n const nested = extractBody(p);\n if (nested) return nested;\n }\n }\n }\n return resp.snippet || '';\n}\n\nconst body = extractBody(resp.payload);\nconst maxLen = 2000;\n\nreturn {\n messageId: resp.id,\n threadId: resp.threadId || '',\n labelIds: resp.labelIds || [],\n from: getH('From'),\n to: getH('To'),\n subject: getH('Subject'),\n date: getH('Date'),\n snippet: resp.snippet || body.substring(0, 100),\n body: body.length > maxLen ? body.substring(0, maxLen) + '...' : body,\n historyId: resp.historyId || '',\n internalDate: resp.internalDate || ''\n};" + } + } + ] + } + }, + { + "id": "formatResult", + "name": "Compile final result", + "type": "code", + "code": { + "source": "const iterations = $.steps.fetchMessages?.response?.iterations || [];\nconst emails = iterations.map(r => r?.parseMessage?.output).filter(Boolean);\nconst nextPageToken = $.steps.checkResults.output.nextPageToken;\nconst q = $.steps.buildQuery.output.q;\nconst max = $.steps.buildQuery.output.max;\n\nconst result = {\n emails,\n totalFound: emails.length,\n requestedCount: max,\n query: q || 'No query specified',\n summary: `Retrieved ${emails.length} email${emails.length === 1 ? '' : 's'}${q ? ' matching \"' + q + '\"' : ''}`\n};\n\nif (nextPageToken) result.nextPageToken = nextPageToken;\n\nreturn result;" + } + } + ] +} diff --git a/flows/gmail/gmail-send-email.flow.json b/flows/gmail/gmail-send-email.flow.json new file mode 100644 index 0000000..476101c --- /dev/null +++ b/flows/gmail/gmail-send-email.flow.json @@ -0,0 +1,111 @@ +{ + "key": "gmail-send-email", + "name": "Send Email via Gmail", + "description": "Compose and send an email through Gmail. Handles MIME encoding, base64url conversion, HTML formatting, CC/BCC, and reply threading automatically.", + "version": "1", + "inputs": { + "gmailConnectionKey": { + "type": "string", + "required": true, + "description": "Gmail connection key", + "connection": { "platform": "gmail" } + }, + "to": { + "type": "string", + "required": true, + "description": "Recipient email address(es), comma-separated" + }, + "subject": { + "type": "string", + "required": true, + "description": "Email subject line" + }, + "body": { + "type": "string", + "required": true, + "description": "Email body (plain text or HTML)" + }, + "isHtml": { + "type": "boolean", + "required": false, + "default": false, + "description": "Set true if body is already HTML. Otherwise plain text is auto-converted." + }, + "from": { + "type": "string", + "required": false, + "description": "Sender address (defaults to authenticated user)" + }, + "cc": { + "type": "string", + "required": false, + "description": "CC recipients, comma-separated" + }, + "bcc": { + "type": "string", + "required": false, + "description": "BCC recipients, comma-separated" + }, + "replyTo": { + "type": "string", + "required": false, + "description": "Reply-To address" + }, + "threadId": { + "type": "string", + "required": false, + "description": "Thread ID to attach this email to (for replies)" + }, + "inReplyTo": { + "type": "string", + "required": false, + "description": "Message-ID of the email being replied to" + }, + "references": { + "type": "string", + "required": false, + "description": "Space-separated chain of Message-IDs for threading" + }, + "labelIds": { + "type": "array", + "required": false, + "description": "Label IDs to apply. Defaults to ['INBOX', 'UNREAD']." + } + }, + "steps": [ + { + "id": "buildMimeMessage", + "name": "Build MIME message and base64url-encode it", + "type": "code", + "code": { + "source": "const { to, subject, body, isHtml, from, cc, bcc, replyTo, inReplyTo, references } = $.input;\n\nif (!to || !subject || !body) throw new Error('to, subject, and body are required');\n\n// Encode non-ASCII subjects per RFC 2047\nfunction encodeSubject(s) {\n if (!/[^\\x00-\\x7F]/.test(s)) return s;\n const bytes = new TextEncoder().encode(s);\n let binary = '';\n bytes.forEach(b => binary += String.fromCharCode(b));\n return '=?UTF-8?B?' + btoa(binary) + '?=';\n}\n\n// Normalize literal \\n sequences to real newlines, then convert to HTML\nfunction textToHtml(text) {\n const normalized = text.replace(/\\\\n/g, '\\n');\n const escaped = normalized.replace(/&/g, '&').replace(//g, '>');\n return '
' + escaped.replace(/\\n/g, '
') + '
';\n}\n\nconst htmlBody = isHtml ? body : textToHtml(body);\n\n// Build MIME headers\nconst headers = [\n 'MIME-Version: 1.0',\n 'From: ' + (from || 'me'),\n 'To: ' + to,\n 'Subject: ' + encodeSubject(subject),\n 'Content-Type: text/html; charset=UTF-8'\n];\n\nif (cc) headers.push('Cc: ' + cc);\nif (bcc) headers.push('Bcc: ' + bcc);\nif (replyTo) headers.push('Reply-To: ' + replyTo);\nif (inReplyTo) headers.push('In-Reply-To: ' + inReplyTo);\nif (references) headers.push('References: ' + references);\n\n// Join with CRLF, add blank line, then body\nconst mimeMessage = headers.join('\\r\\n') + '\\r\\n\\r\\n' + htmlBody;\n\n// Base64url encode\nconst encoder = new TextEncoder();\nconst data = encoder.encode(mimeMessage);\nlet binary = '';\nfor (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]);\nconst base64 = btoa(binary);\nconst base64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\nconst labelIds = $.input.labelIds && $.input.labelIds.length > 0 ? $.input.labelIds : ['INBOX', 'UNREAD'];\nconst threadId = $.input.threadId || undefined;\n\nreturn { raw: base64url, labelIds, threadId };" + } + }, + { + "id": "sendEmail", + "name": "Send the encoded email via Gmail API", + "type": "action", + "action": { + "platform": "gmail", + "actionId": "conn_mod_def::GJ3odhCpd3I::gujvYoneSk6NFWltse9bGg", + "connectionKey": "$.input.gmailConnectionKey", + "pathVars": { + "userId": "me" + }, + "data": { + "raw": "$.steps.buildMimeMessage.output.raw", + "labelIds": "$.steps.buildMimeMessage.output.labelIds", + "threadId": "$.steps.buildMimeMessage.output.threadId" + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.sendEmail.response || {};\nconst to = $.input.to;\nconst cc = $.input.cc;\nconst bcc = $.input.bcc;\n\nconst toList = to.split(',').map(e => e.trim()).filter(Boolean);\nconst ccList = cc ? cc.split(',').map(e => e.trim()).filter(Boolean) : [];\nconst bccList = bcc ? bcc.split(',').map(e => e.trim()).filter(Boolean) : [];\nconst total = toList.length + ccList.length + bccList.length;\n\nreturn {\n messageId: resp.id || '',\n threadId: resp.threadId || '',\n labelIds: resp.labelIds || [],\n recipients: { to: toList, cc: ccList, bcc: bccList },\n subject: $.input.subject,\n sent: !!resp.id,\n summary: resp.id\n ? `Sent \"${$.input.subject}\" to ${total} recipient${total === 1 ? '' : 's'}`\n : 'Failed to send email'\n};" + } + } + ] +} diff --git a/flows/google-calendar/README.md b/flows/google-calendar/README.md new file mode 100644 index 0000000..7830b24 --- /dev/null +++ b/flows/google-calendar/README.md @@ -0,0 +1,86 @@ +--- +name: google-calendar +description: | + Google Calendar integration flow for the One CLI. Create calendar events + with attendees, location, reminders, and all-day event support. +triggers: + - "google calendar" + - "create event" + - "calendar event" + - "schedule meeting" + - "/google-calendar" +--- + +# Google Calendar Flows + +Ready-to-run workflows for Google Calendar via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add google-calendar # Connect your Google account +one --agent list # Find your connection key +``` + +## Discovery + +The `calendarId` defaults to `'primary'` (your main calendar), which works for most cases. To use a different calendar: + +```bash +# List all calendars on your account +one --agent actions search google-calendar "list calendars" +one --agent actions execute google-calendar +``` + +## Flows + +### Create Event + +Create a new event on Google Calendar with full support for attendees, location, all-day events, and notification preferences. + +```bash +one flow execute google-calendar-create-event.flow.json \ + --input googleCalendarConnectionKey="" \ + --input summary="Team Standup" \ + --input startDateTime="2024-03-15T10:00:00-07:00" \ + --input endDateTime="2024-03-15T10:30:00-07:00" \ + --input attendees="alice@example.com,bob@example.com" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `googleCalendarConnectionKey` | Yes | Google Calendar connection key | +| `calendarId` | No | Calendar ID (default: 'primary') | +| `summary` | Yes | Event title | +| `description` | No | Event description (supports HTML) | +| `location` | No | Event location | +| `startDateTime` | Yes | Start (ISO 8601 datetime or YYYY-MM-DD for all-day) | +| `endDateTime` | Yes | End (ISO 8601 datetime or YYYY-MM-DD for all-day) | +| `timeZone` | No | Time zone (e.g., 'America/Los_Angeles') | +| `attendees` | No | Comma-separated attendee emails | +| `sendUpdates` | No | 'all', 'externalOnly', or 'none' (default) | + +**What it does under the hood:** + +1. Detects all-day events (date-only format) vs timed events +2. Builds event object with start/end, attendees, location +3. Creates event via `POST /v3/calendars/{calendarId}/events` + +**All-day event example:** + +```bash +one flow execute google-calendar-create-event.flow.json \ + --input googleCalendarConnectionKey="" \ + --input summary="Company Offsite" \ + --input startDateTime="2024-03-15" \ + --input endDateTime="2024-03-17" +``` + +## Adapting These Flows + +- **Meeting from email**: Chain `gmail-read-emails` into this flow to auto-schedule meetings from email content. +- **Recurring sync**: Use the list events action to detect conflicts before creating. +- **Calendar + CRM**: Create an event and log it as an activity in HubSpot or ActiveCampaign. diff --git a/flows/google-calendar/google-calendar-create-event.flow.json b/flows/google-calendar/google-calendar-create-event.flow.json new file mode 100644 index 0000000..5a0fa93 --- /dev/null +++ b/flows/google-calendar/google-calendar-create-event.flow.json @@ -0,0 +1,94 @@ +{ + "key": "google-calendar-create-event", + "name": "Create a Google Calendar Event", + "description": "Create an event on Google Calendar with support for attendees, location, description, reminders, and all-day events.", + "version": "1", + "inputs": { + "googleCalendarConnectionKey": { + "type": "string", + "required": true, + "description": "Google Calendar connection key", + "connection": { "platform": "google-calendar" } + }, + "calendarId": { + "type": "string", + "required": false, + "default": "primary", + "description": "Calendar ID (default: 'primary')" + }, + "summary": { + "type": "string", + "required": true, + "description": "Event title" + }, + "description": { + "type": "string", + "required": false, + "description": "Event description (supports HTML)" + }, + "location": { + "type": "string", + "required": false, + "description": "Event location" + }, + "startDateTime": { + "type": "string", + "required": true, + "description": "Start time in ISO 8601 format (e.g., '2024-03-15T10:00:00-07:00') or date for all-day events ('2024-03-15')" + }, + "endDateTime": { + "type": "string", + "required": true, + "description": "End time in ISO 8601 format or date for all-day events" + }, + "timeZone": { + "type": "string", + "required": false, + "description": "Time zone (e.g., 'America/Los_Angeles'). Defaults to calendar's time zone." + }, + "attendees": { + "type": "string", + "required": false, + "description": "Comma-separated email addresses of attendees" + }, + "sendUpdates": { + "type": "string", + "required": false, + "default": "none", + "description": "Whether to send notifications: 'all', 'externalOnly', or 'none'" + } + }, + "steps": [ + { + "id": "buildEvent", + "name": "Build event payload", + "type": "code", + "code": { + "source": "const { summary, description, location, startDateTime, endDateTime, timeZone, attendees, sendUpdates } = $.input;\nif (!summary || !startDateTime || !endDateTime) throw new Error('summary, startDateTime, and endDateTime are required');\n\nconst isAllDay = /^\\d{4}-\\d{2}-\\d{2}$/.test(startDateTime);\nconst event = { summary };\n\nif (description) event.description = description;\nif (location) event.location = location;\n\nif (isAllDay) {\n event.start = { date: startDateTime };\n event.end = { date: endDateTime };\n} else {\n event.start = { dateTime: startDateTime };\n event.end = { dateTime: endDateTime };\n if (timeZone) {\n event.start.timeZone = timeZone;\n event.end.timeZone = timeZone;\n }\n}\n\nif (attendees) {\n event.attendees = attendees.split(',').map(e => ({ email: e.trim() })).filter(a => a.email);\n}\n\nconst queryParams = {};\nif (sendUpdates) queryParams.sendUpdates = sendUpdates;\n\nreturn { event, queryParams };" + } + }, + { + "id": "createEvent", + "name": "Create event via Google Calendar API", + "type": "action", + "action": { + "platform": "google-calendar", + "actionId": "conn_mod_def::GJ6RlnjZAh4::CSya4eHtRbeXRM7PHiXuRA", + "connectionKey": "$.input.googleCalendarConnectionKey", + "pathVars": { + "calendarId": "$.input.calendarId" + }, + "queryParams": "$.steps.buildEvent.output.queryParams", + "data": "$.steps.buildEvent.output.event" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createEvent.response || {};\nreturn {\n eventId: resp.id || '',\n htmlLink: resp.htmlLink || '',\n summary: resp.summary || $.input.summary,\n start: resp.start || {},\n end: resp.end || {},\n status: resp.status || '',\n created: !!resp.id,\n result: resp.id ? `Created event \"${resp.summary || $.input.summary}\"` : 'Failed to create event'\n};" + } + } + ] +} diff --git a/flows/google-docs/README.md b/flows/google-docs/README.md new file mode 100644 index 0000000..2d5f84b --- /dev/null +++ b/flows/google-docs/README.md @@ -0,0 +1,60 @@ +--- +name: google-docs +description: | + Google Docs integration flow for the One CLI. Create documents with titles + and content. Handles the two-step create-then-update pattern for inserting + body text. +triggers: + - "google docs" + - "create document" + - "create doc" + - "new document" + - "/google-docs" +--- + +# Google Docs Flows + +Ready-to-run workflows for Google Docs via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add google-docs # Connect your Google account +one --agent list # Find your connection key +``` + +## Flows + +### Create Document + +Create a new Google Doc with a title and optional body content. + +```bash +one flow execute google-docs-create-document.flow.json \ + --input googleDocsConnectionKey="" \ + --input title="Meeting Notes - March 15" \ + --input content="Attendees: Alice, Bob\n\nAgenda:\n1. Q1 review\n2. Planning" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `googleDocsConnectionKey` | Yes | Google Docs connection key | +| `title` | Yes | Document title | +| `content` | No | Plain text content for the document body | + +**What it does under the hood:** + +1. Creates a blank document with the title via `POST /v1/documents` +2. If content is provided, inserts text at position 1 via `POST /v1/documents/{documentId}` (batch update) +3. Returns the document ID, title, and edit URL + +**Note:** The Google Docs API requires a two-step pattern: create the document first, then batch-update to insert content. This flow handles that automatically. + +## Adapting These Flows + +- **Meeting notes**: Chain `calendly-list-events` or `google-calendar` into this flow to auto-generate meeting note documents. +- **Rich content**: Replace the text insert with structured batch update requests (headings, lists, tables) using the `children` block format. +- **Templates**: Create a doc, then use batch update to insert content at specific locations. diff --git a/flows/google-docs/google-docs-create-document.flow.json b/flows/google-docs/google-docs-create-document.flow.json new file mode 100644 index 0000000..2daa9c8 --- /dev/null +++ b/flows/google-docs/google-docs-create-document.flow.json @@ -0,0 +1,71 @@ +{ + "key": "google-docs-create-document", + "name": "Create a Google Document", + "description": "Create a new Google Doc with a title and optional body content. Handles the two-step pattern: create the document, then batch-update to insert content.", + "version": "1", + "inputs": { + "googleDocsConnectionKey": { + "type": "string", + "required": true, + "description": "Google Docs connection key", + "connection": { "platform": "google-docs" } + }, + "title": { + "type": "string", + "required": true, + "description": "Document title" + }, + "content": { + "type": "string", + "required": false, + "description": "Plain text content to insert into the document body" + } + }, + "steps": [ + { + "id": "createDoc", + "name": "Create empty Google Doc with title", + "type": "action", + "action": { + "platform": "google-docs", + "actionId": "conn_mod_def::GDa4Clf2NGc::S3KS03muT-2hTeHnkQ_w8g", + "connectionKey": "$.input.googleDocsConnectionKey", + "data": { + "title": "$.input.title" + } + } + }, + { + "id": "insertContent", + "name": "Insert text content into the document", + "type": "action", + "if": "$.input.content", + "action": { + "platform": "google-docs", + "actionId": "conn_mod_def::GDa4EqlqkCk::00RtyUaHQDSsZ_E3h5_c2A", + "connectionKey": "$.input.googleDocsConnectionKey", + "pathVars": { + "documentId": "$.steps.createDoc.response.documentId" + }, + "data": { + "requests": [ + { + "insertText": { + "location": { "index": 1 }, + "text": "$.input.content" + } + } + ] + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const doc = $.steps.createDoc.response || {};\nconst docUrl = doc.documentId ? `https://docs.google.com/document/d/${doc.documentId}/edit` : '';\nreturn {\n documentId: doc.documentId || '',\n title: doc.title || $.input.title,\n url: docUrl,\n created: !!doc.documentId,\n hasContent: !!$.input.content,\n summary: doc.documentId ? `Created document \"${doc.title || $.input.title}\"` : 'Failed to create document'\n};" + } + } + ] +} diff --git a/flows/google-drive/README.md b/flows/google-drive/README.md new file mode 100644 index 0000000..766ddae --- /dev/null +++ b/flows/google-drive/README.md @@ -0,0 +1,73 @@ +--- +name: google-drive +description: | + Google Drive integration flow for the One CLI. List/search files and create + new files or folders in Google Drive. +triggers: + - "google drive" + - "list files" + - "create folder" + - "search drive" + - "/google-drive" +--- + +# Google Drive Flows + +Ready-to-run workflows for Google Drive via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add google-drive # Connect your Google account +one --agent list # Find your connection key +``` + +## Flows + +### Manage Files + +List/search files or create a new file/folder in Google Drive. A single flow with two operations. + +```bash +# Search for files +one flow execute google-drive-manage-files.flow.json \ + --input googleDriveConnectionKey="" \ + --input operation="list" \ + --input query="name contains 'report'" + +# Create a folder +one flow execute google-drive-manage-files.flow.json \ + --input googleDriveConnectionKey="" \ + --input operation="create" \ + --input name="Project Assets" \ + --input mimeType="application/vnd.google-apps.folder" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `googleDriveConnectionKey` | Yes | Google Drive connection key | +| `operation` | Yes | 'list' or 'create' | +| `query` | No | Drive search query (list operation) | +| `name` | No | File/folder name (create operation) | +| `mimeType` | No | MIME type. Use `application/vnd.google-apps.folder` for folders | +| `parents` | No | Parent folder IDs array (create operation) | +| `pageSize` | No | Results per page (default 20, max 1000) | + +**Drive search query examples:** + +| Query | Meaning | +|-------|---------| +| `name contains 'report'` | Files with "report" in the name | +| `mimeType='application/vnd.google-apps.folder'` | Only folders | +| `modifiedTime > '2024-01-01'` | Modified after a date | +| `'folderId' in parents` | Files in a specific folder | +| `trashed = false` | Non-trashed files only | + +## Adapting These Flows + +- **Organize uploads**: Create a folder, then move or copy files into it. +- **Backup pipeline**: List files from one folder and copy them to another. +- **Drive + Sheets**: Create a Google Sheets file (mimeType: `application/vnd.google-apps.spreadsheet`) and then use the Sheets flow to populate it. diff --git a/flows/google-drive/google-drive-manage-files.flow.json b/flows/google-drive/google-drive-manage-files.flow.json new file mode 100644 index 0000000..c3d4066 --- /dev/null +++ b/flows/google-drive/google-drive-manage-files.flow.json @@ -0,0 +1,95 @@ +{ + "key": "google-drive-manage-files", + "name": "List and Create Files in Google Drive", + "description": "Search for files in Google Drive or create a new file/folder. Supports Drive search query syntax for filtering.", + "version": "1", + "inputs": { + "googleDriveConnectionKey": { + "type": "string", + "required": true, + "description": "Google Drive connection key", + "connection": { "platform": "google-drive" } + }, + "operation": { + "type": "string", + "required": true, + "description": "Operation: 'list' to search/list files, 'create' to create a file or folder" + }, + "query": { + "type": "string", + "required": false, + "description": "Drive search query for list operation (e.g., \"name contains 'report'\" or \"mimeType='application/vnd.google-apps.folder'\")" + }, + "name": { + "type": "string", + "required": false, + "description": "File or folder name (for create operation)" + }, + "mimeType": { + "type": "string", + "required": false, + "description": "MIME type. Use 'application/vnd.google-apps.folder' for folders." + }, + "parents": { + "type": "array", + "required": false, + "description": "Parent folder IDs (for create operation)" + }, + "pageSize": { + "type": "number", + "required": false, + "default": 20, + "description": "Number of files to return (list operation, max 1000)" + } + }, + "steps": [ + { + "id": "routeOperation", + "name": "Determine which operation to run", + "type": "code", + "code": { + "source": "const op = ($.input.operation || '').toLowerCase();\nif (op !== 'list' && op !== 'create') throw new Error('operation must be \"list\" or \"create\"');\nreturn { op };" + } + }, + { + "id": "listFiles", + "name": "List/search files in Google Drive", + "type": "action", + "if": "$.steps.routeOperation.output.op === 'list'", + "action": { + "platform": "google-drive", + "actionId": "conn_mod_def::GJ6Rzy_a8J8::5DPVGp3fTXegRgMN4v11tA", + "connectionKey": "$.input.googleDriveConnectionKey", + "queryParams": { + "q": "$.input.query", + "pageSize": "$.input.pageSize", + "fields": "files(id,name,mimeType,createdTime,modifiedTime,size,webViewLink,parents),nextPageToken" + } + } + }, + { + "id": "createFile", + "name": "Create a file or folder", + "type": "action", + "if": "$.steps.routeOperation.output.op === 'create'", + "action": { + "platform": "google-drive", + "actionId": "conn_mod_def::GJ6RzlNn1fs::1Qlp0KgqQbGF7WVXdC5wsw", + "connectionKey": "$.input.googleDriveConnectionKey", + "data": { + "name": "$.input.name", + "mimeType": "$.input.mimeType", + "parents": "$.input.parents" + } + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const op = $.steps.routeOperation.output.op;\nif (op === 'list') {\n const resp = $.steps.listFiles?.response || {};\n const files = resp.files || [];\n return {\n files: files.map(f => ({ id: f.id, name: f.name, mimeType: f.mimeType, modifiedTime: f.modifiedTime, webViewLink: f.webViewLink })),\n count: files.length,\n nextPageToken: resp.nextPageToken || null,\n summary: `Found ${files.length} file${files.length === 1 ? '' : 's'}`\n };\n} else {\n const resp = $.steps.createFile?.response || {};\n const isFolder = (resp.mimeType || '').includes('folder');\n return {\n id: resp.id || '',\n name: resp.name || $.input.name,\n mimeType: resp.mimeType || '',\n webViewLink: resp.webViewLink || '',\n created: !!resp.id,\n summary: resp.id ? `Created ${isFolder ? 'folder' : 'file'} \"${resp.name || $.input.name}\"` : 'Failed to create'\n };\n}" + } + } + ] +} diff --git a/flows/google-sheets/README.md b/flows/google-sheets/README.md new file mode 100644 index 0000000..4733560 --- /dev/null +++ b/flows/google-sheets/README.md @@ -0,0 +1,77 @@ +--- +name: google-sheets +description: | + Google Sheets integration flow for the One CLI. Append rows of data to a + spreadsheet with automatic value formatting and insert-after-last-row + semantics. +triggers: + - "google sheets" + - "append rows" + - "add to spreadsheet" + - "spreadsheet" + - "/google-sheets" +--- + +# Google Sheets Flows + +Ready-to-run workflows for Google Sheets via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add google-sheets # Connect your Google account +one --agent list # Find your connection key +``` + +## Discovery + +You need a `spreadsheetId` to use these flows. Find it from: + +- **URL**: Open the spreadsheet in your browser. The ID is in the URL: `docs.google.com/spreadsheets/d//edit` +- **Google Drive search**: Use the [Google Drive skill](../google-drive) to search for spreadsheets: + ```bash + one flow execute google-drive-manage-files.flow.json \ + --input googleDriveConnectionKey="" \ + --input operation="list" \ + --input query="mimeType='application/vnd.google-apps.spreadsheet'" + ``` + +## Flows + +### Append Rows + +Append one or more rows to a Google Sheets spreadsheet. Data is inserted after the last row containing data. + +```bash +one flow execute google-sheets-append-rows.flow.json \ + --input googleSheetsConnectionKey="" \ + --input spreadsheetId="1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms" \ + --input range="Sheet1" \ + --input values='[["Name","Email","Date"],["Alice","alice@example.com","2024-03-15"]]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `googleSheetsConnectionKey` | Yes | Google Sheets connection key | +| `spreadsheetId` | Yes | Spreadsheet ID (from URL) | +| `range` | No | Sheet name or A1 range (default: 'Sheet1') | +| `values` | Yes | 2D array of values (each inner array = one row) | +| `valueInputOption` | No | 'USER_ENTERED' (default, parsed) or 'RAW' (stored as-is) | + +**What it does under the hood:** + +1. Validates and formats the 2D values array +2. Appends via `POST /v4/spreadsheets/{id}/values/{range}:append` +3. Uses `INSERT_ROWS` to push data after existing content +4. Returns the updated range, row count, and cell count + +**Tip:** Use `USER_ENTERED` (default) to let Sheets parse dates, numbers, and formulas. Use `RAW` to store values exactly as provided. + +## Adapting These Flows + +- **Data logger**: Pipe output from any flow (email summaries, CRM contacts, calendar events) into a tracking spreadsheet. +- **CSV import**: Parse CSV data into a 2D array and append it in batches. +- **Read + write**: Use the get-values action to read existing data, process it, and write results back. diff --git a/flows/google-sheets/google-sheets-append-rows.flow.json b/flows/google-sheets/google-sheets-append-rows.flow.json new file mode 100644 index 0000000..09e8755 --- /dev/null +++ b/flows/google-sheets/google-sheets-append-rows.flow.json @@ -0,0 +1,70 @@ +{ + "key": "google-sheets-append-rows", + "name": "Append Rows to Google Sheets", + "description": "Append rows of data to a Google Sheets spreadsheet. Handles the value range formatting and append semantics (inserts after the last row with data).", + "version": "1", + "inputs": { + "googleSheetsConnectionKey": { + "type": "string", + "required": true, + "description": "Google Sheets connection key", + "connection": { "platform": "google-sheets" } + }, + "spreadsheetId": { + "type": "string", + "required": true, + "description": "Spreadsheet ID (from the URL)" + }, + "range": { + "type": "string", + "required": false, + "default": "Sheet1", + "description": "Sheet name or A1 range notation (e.g., 'Sheet1' or 'Sheet1!A:Z')" + }, + "values": { + "type": "array", + "required": true, + "description": "2D array of values. Each inner array is a row. Example: [[\"Name\",\"Email\"],[\"Alice\",\"alice@example.com\"]]" + }, + "valueInputOption": { + "type": "string", + "required": false, + "default": "USER_ENTERED", + "description": "'USER_ENTERED' (parsed like typing in the UI) or 'RAW' (stored as-is)" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Validate and build request", + "type": "code", + "code": { + "source": "const { values, range, valueInputOption } = $.input;\nif (!values || !Array.isArray(values) || values.length === 0) throw new Error('values must be a non-empty 2D array');\nconst formatted = values.map(row => Array.isArray(row) ? row : [row]);\nreturn {\n body: { range: range || 'Sheet1', majorDimension: 'ROWS', values: formatted },\n queryParams: { valueInputOption: valueInputOption || 'USER_ENTERED', insertDataOption: 'INSERT_ROWS' }\n};" + } + }, + { + "id": "appendValues", + "name": "Append values via Sheets API", + "type": "action", + "action": { + "platform": "google-sheets", + "actionId": "conn_mod_def::GJ30kKk8ogk::hCE5XVrgQ3m0ip3lGzJRfQ", + "connectionKey": "$.input.googleSheetsConnectionKey", + "pathVars": { + "spreadsheetId": "$.input.spreadsheetId", + "range": "$.input.range" + }, + "queryParams": "$.steps.buildPayload.output.queryParams", + "data": "$.steps.buildPayload.output.body" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.appendValues.response || {};\nconst updates = resp.updates || {};\nreturn {\n spreadsheetId: resp.spreadsheetId || $.input.spreadsheetId,\n updatedRange: updates.updatedRange || resp.tableRange || '',\n updatedRows: updates.updatedRows || 0,\n updatedColumns: updates.updatedColumns || 0,\n updatedCells: updates.updatedCells || 0,\n summary: `Appended ${updates.updatedRows || 0} row${(updates.updatedRows || 0) === 1 ? '' : 's'} to ${$.input.range || 'Sheet1'}`\n};" + } + } + ] +} diff --git a/flows/hubspot/README.md b/flows/hubspot/README.md new file mode 100644 index 0000000..bb031a8 --- /dev/null +++ b/flows/hubspot/README.md @@ -0,0 +1,112 @@ +--- +name: hubspot +description: | + HubSpot CRM integration flows for the One CLI. Create contacts, search + contacts, and search any CRM object type (companies, deals, tickets) with + filters and property selection. +triggers: + - "hubspot" + - "create contact hubspot" + - "search contacts hubspot" + - "search deals hubspot" + - "crm search" + - "/hubspot" +--- + +# HubSpot Flows + +Ready-to-run workflows for HubSpot CRM via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add hubspot # Connect your HubSpot account +one --agent list # Find your connection key +``` + +## Flows + +### Create Contact + +Create a new contact with standard and custom properties. + +```bash +one flow execute hubspot-create-contact.flow.json \ + --input hubspotConnectionKey="" \ + --input email="alice@example.com" \ + --input firstName="Alice" \ + --input lastName="Smith" \ + --input company="Acme Corp" \ + --input lifecycleStage="lead" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `hubspotConnectionKey` | Yes | HubSpot connection key | +| `email` | Yes | Contact email | +| `firstName` | No | First name | +| `lastName` | No | Last name | +| `phone` | No | Phone number | +| `company` | No | Company name | +| `jobTitle` | No | Job title | +| `lifecycleStage` | No | 'subscriber', 'lead', 'opportunity', 'customer' | +| `customProperties` | No | Object of additional properties (string values) | + +### Search Contacts + +Search HubSpot contacts by free text or property filters. + +```bash +one flow execute hubspot-search-contacts.flow.json \ + --input hubspotConnectionKey="" \ + --input searchTerm="alice" \ + --input limit=5 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `hubspotConnectionKey` | Yes | HubSpot connection key | +| `searchTerm` | No | Free-text search | +| `filterProperty` | No | Property to filter by | +| `filterOperator` | No | Operator: 'EQ', 'CONTAINS', 'GT', 'LT', etc. | +| `filterValue` | No | Filter value | +| `properties` | No | Properties to return (default: email, name, phone, company) | +| `limit` | No | Results (max 100, default 10) | + +### Search CRM Objects + +Search any HubSpot CRM object type: contacts, companies, deals, tickets, or custom objects. + +```bash +one flow execute hubspot-search-crm-objects.flow.json \ + --input hubspotConnectionKey="" \ + --input objectType="deals" \ + --input filterProperty="dealstage" \ + --input filterOperator="EQ" \ + --input filterValue="closedwon" \ + --input properties='["dealname","amount","closedate"]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `hubspotConnectionKey` | Yes | HubSpot connection key | +| `objectType` | Yes | 'contacts', 'companies', 'deals', 'tickets', or custom type | +| `searchTerm` | No | Free-text search | +| `filterProperty` | No | Property to filter by | +| `filterOperator` | No | Operator: 'EQ', 'CONTAINS', 'GT', 'LT', etc. | +| `filterValue` | No | Filter value | +| `properties` | No | Properties to return | +| `limit` | No | Results (max 100, default 10) | + +## Adapting These Flows + +- **Lead capture**: Chain `gmail-read-emails` into `hubspot-create-contact` to auto-create contacts from inbound emails. +- **Deal pipeline**: Search contacts, then create deals associated with them. +- **Reporting**: Search deals by stage and pipe into Google Sheets for dashboard tracking. diff --git a/flows/hubspot/hubspot-create-contact.flow.json b/flows/hubspot/hubspot-create-contact.flow.json new file mode 100644 index 0000000..49ddc58 --- /dev/null +++ b/flows/hubspot/hubspot-create-contact.flow.json @@ -0,0 +1,83 @@ +{ + "key": "hubspot-create-contact", + "name": "Create a Contact in HubSpot", + "description": "Create a new contact in HubSpot CRM with standard and custom properties. Supports optional association with a company.", + "version": "1", + "inputs": { + "hubspotConnectionKey": { + "type": "string", + "required": true, + "description": "HubSpot connection key", + "connection": { "platform": "hubspot" } + }, + "email": { + "type": "string", + "required": true, + "description": "Contact email address" + }, + "firstName": { + "type": "string", + "required": false, + "description": "First name" + }, + "lastName": { + "type": "string", + "required": false, + "description": "Last name" + }, + "phone": { + "type": "string", + "required": false, + "description": "Phone number" + }, + "company": { + "type": "string", + "required": false, + "description": "Company name" + }, + "jobTitle": { + "type": "string", + "required": false, + "description": "Job title" + }, + "lifecycleStage": { + "type": "string", + "required": false, + "description": "Lifecycle stage (e.g., 'subscriber', 'lead', 'opportunity', 'customer')" + }, + "customProperties": { + "type": "object", + "required": false, + "description": "Additional custom properties as key-value pairs (all values must be strings)" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build contact payload", + "type": "code", + "code": { + "source": "const { email, firstName, lastName, phone, company, jobTitle, lifecycleStage, customProperties } = $.input;\nif (!email) throw new Error('email is required');\nconst properties = { email };\nif (firstName) properties.firstname = firstName;\nif (lastName) properties.lastname = lastName;\nif (phone) properties.phone = phone;\nif (company) properties.company = company;\nif (jobTitle) properties.jobtitle = jobTitle;\nif (lifecycleStage) properties.lifecyclestage = lifecycleStage;\nif (customProperties) Object.assign(properties, customProperties);\nreturn { properties };" + } + }, + { + "id": "createContact", + "name": "Create contact via HubSpot API", + "type": "action", + "action": { + "platform": "hubspot", + "actionId": "conn_mod_def::GJ3kRa59YdQ::k6o-IYauSoqishpRytOX-Q", + "connectionKey": "$.input.hubspotConnectionKey", + "data": "$.steps.buildPayload.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createContact.response || {};\nreturn {\n contactId: resp.id || '',\n properties: resp.properties || {},\n createdAt: resp.createdAt || '',\n created: !!resp.id,\n summary: resp.id\n ? `Created contact ${resp.properties?.email || $.input.email} (ID: ${resp.id})`\n : 'Failed to create contact'\n};" + } + } + ] +} diff --git a/flows/hubspot/hubspot-search-contacts.flow.json b/flows/hubspot/hubspot-search-contacts.flow.json new file mode 100644 index 0000000..e568bfc --- /dev/null +++ b/flows/hubspot/hubspot-search-contacts.flow.json @@ -0,0 +1,75 @@ +{ + "key": "hubspot-search-contacts", + "name": "Search Contacts in HubSpot", + "description": "Search HubSpot CRM contacts using filters (email, name, company, etc.) with support for property selection and sorting.", + "version": "1", + "inputs": { + "hubspotConnectionKey": { + "type": "string", + "required": true, + "description": "HubSpot connection key", + "connection": { "platform": "hubspot" } + }, + "searchTerm": { + "type": "string", + "required": false, + "description": "Free-text search term (searches across default searchable properties)" + }, + "filterProperty": { + "type": "string", + "required": false, + "description": "Property name to filter by (e.g., 'email', 'company', 'lifecyclestage')" + }, + "filterOperator": { + "type": "string", + "required": false, + "default": "EQ", + "description": "Filter operator: 'EQ', 'NEQ', 'CONTAINS', 'GT', 'LT', 'GTE', 'LTE', 'HAS_PROPERTY', etc." + }, + "filterValue": { + "type": "string", + "required": false, + "description": "Value to filter by" + }, + "properties": { + "type": "array", + "required": false, + "description": "Properties to return. Default: ['email', 'firstname', 'lastname', 'phone', 'company']" + }, + "limit": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of results (max 100)" + } + }, + "steps": [ + { + "id": "buildSearch", + "name": "Build search request", + "type": "code", + "code": { + "source": "const { searchTerm, filterProperty, filterOperator, filterValue, properties, limit } = $.input;\nconst body = { limit: Math.min(limit || 10, 100) };\nbody.properties = properties || ['email', 'firstname', 'lastname', 'phone', 'company', 'lifecyclestage'];\nif (searchTerm) body.query = searchTerm;\nif (filterProperty && filterValue) {\n body.filterGroups = [{\n filters: [{\n propertyName: filterProperty,\n operator: filterOperator || 'EQ',\n value: filterValue\n }]\n }];\n}\nreturn body;" + } + }, + { + "id": "searchContacts", + "name": "Search contacts via HubSpot API", + "type": "action", + "action": { + "platform": "hubspot", + "actionId": "conn_mod_def::GJ3kSC2Sp7I::L6cYnwOfTUatg0cXeWjCBA", + "connectionKey": "$.input.hubspotConnectionKey", + "data": "$.steps.buildSearch.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.searchContacts.response || {};\nconst results = resp.results || [];\nreturn {\n contacts: results.map(c => ({\n id: c.id,\n properties: c.properties || {},\n createdAt: c.createdAt,\n updatedAt: c.updatedAt\n })),\n total: resp.total || results.length,\n count: results.length,\n paging: resp.paging || null,\n summary: `Found ${resp.total || results.length} contact${(resp.total || results.length) === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/hubspot/hubspot-search-crm-objects.flow.json b/flows/hubspot/hubspot-search-crm-objects.flow.json new file mode 100644 index 0000000..b382808 --- /dev/null +++ b/flows/hubspot/hubspot-search-crm-objects.flow.json @@ -0,0 +1,83 @@ +{ + "key": "hubspot-search-crm-objects", + "name": "Search CRM Objects in HubSpot", + "description": "Search any HubSpot CRM object type (contacts, companies, deals, tickets, etc.) using filters, free-text search, and property selection.", + "version": "1", + "inputs": { + "hubspotConnectionKey": { + "type": "string", + "required": true, + "description": "HubSpot connection key", + "connection": { "platform": "hubspot" } + }, + "objectType": { + "type": "string", + "required": true, + "description": "CRM object type: 'contacts', 'companies', 'deals', 'tickets', or a custom object type" + }, + "searchTerm": { + "type": "string", + "required": false, + "description": "Free-text search term" + }, + "filterProperty": { + "type": "string", + "required": false, + "description": "Property name to filter by" + }, + "filterOperator": { + "type": "string", + "required": false, + "default": "EQ", + "description": "Filter operator: 'EQ', 'NEQ', 'CONTAINS', 'GT', 'LT', 'GTE', 'LTE'" + }, + "filterValue": { + "type": "string", + "required": false, + "description": "Value to filter by" + }, + "properties": { + "type": "array", + "required": false, + "description": "Properties to return in results" + }, + "limit": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of results (max 100)" + } + }, + "steps": [ + { + "id": "buildSearch", + "name": "Build search payload", + "type": "code", + "code": { + "source": "const { objectType, searchTerm, filterProperty, filterOperator, filterValue, properties, limit } = $.input;\nif (!objectType) throw new Error('objectType is required');\nconst body = { limit: Math.min(limit || 10, 100) };\nif (searchTerm) body.query = searchTerm;\nif (properties && properties.length > 0) body.properties = properties;\nif (filterProperty && filterValue) {\n body.filterGroups = [{\n filters: [{\n propertyName: filterProperty,\n operator: filterOperator || 'EQ',\n value: filterValue\n }]\n }];\n}\nreturn body;" + } + }, + { + "id": "searchObjects", + "name": "Search CRM objects via HubSpot API", + "type": "action", + "action": { + "platform": "hubspot", + "actionId": "conn_mod_def::GJ3kO2fhBr4::VOH0GD9xT-WtubhgYB2qWw", + "connectionKey": "$.input.hubspotConnectionKey", + "pathVars": { + "objectType": "$.input.objectType" + }, + "data": "$.steps.buildSearch.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.searchObjects.response || {};\nconst results = resp.results || [];\nreturn {\n objects: results.map(o => ({\n id: o.id,\n properties: o.properties || {},\n createdAt: o.createdAt,\n updatedAt: o.updatedAt\n })),\n objectType: $.input.objectType,\n total: resp.total || results.length,\n count: results.length,\n paging: resp.paging || null,\n summary: `Found ${resp.total || results.length} ${$.input.objectType} record${(resp.total || results.length) === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/jira/README.md b/flows/jira/README.md new file mode 100644 index 0000000..de61281 --- /dev/null +++ b/flows/jira/README.md @@ -0,0 +1,249 @@ +--- +name: jira +description: | + Jira Cloud integration flows for the One CLI. Ready-to-run workflows that + handle orchestration complexity (Atlassian Document Format encoding, JQL + search, two-step transition lookups) so you don't have to. +triggers: + - "create issue" + - "create ticket" + - "search issues" + - "jql search" + - "transition issue" + - "move issue" + - "add comment" + - "get issue" + - "jira" + - "/jira" +--- + +# Jira Flows + +Ready-to-run workflows for Jira Cloud via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add jira # Connect your Jira Cloud account +one --agent list # Find your connection key +``` + +## Discovery + +Jira flows require a Cloud ID and project/issue type IDs. Find them with: + +```bash +# Get your Jira Cloud ID (a UUID identifying your Jira site) +one --agent actions search jira "get sites" +one --agent actions execute jira + +# List projects (to find projectKey like "ENG") +one --agent actions search jira "get projects" +one --agent actions execute jira \ + --path-vars '{"cloudId":""}' + +# List issue types (to find issueTypeId like "10001" for Task) +one --agent actions search jira "get issue types" +one --agent actions execute jira \ + --path-vars '{"cloudId":""}' +``` + +## Flows + +### Create Issue + +Creates a Jira issue with proper Atlassian Document Format (ADF) for the +description. Handles project lookup by key, ADF encoding, labels, priority, +and optional subtask creation. + +```bash +one flow execute jira-create-issue.flow.json \ + --input jiraConnectionKey="" \ + --input jiraCloudId="" \ + --input projectKey="ENG" \ + --input issueTypeId="10001" \ + --input summary="API rate limiting not enforced" \ + --input description="The /api/v2/users endpoint has no rate limiting.\nThis allows abuse." \ + --input priority="High" \ + --input labels='["backend","security"]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `jiraConnectionKey` | Yes | Your Jira connection key | +| `jiraCloudId` | Yes | Jira Cloud site ID (UUID) | +| `projectKey` | Yes | Project key (e.g., `ENG`) | +| `issueTypeId` | Yes | Issue type ID (e.g., `10001` for Task) | +| `summary` | Yes | Issue title | +| `description` | No | Plain text, auto-converted to ADF | +| `priority` | No | Priority name (`High`, `Medium`, `Low`) | +| `labels` | No | Array of label strings | +| `assigneeAccountId` | No | Atlassian account ID of the assignee | +| `parentKey` | No | Parent issue key for subtask creation | + +**What it does under the hood:** + +1. Converts plain text description to Atlassian Document Format (ADF) +2. Builds the `fields` payload with project key, issue type, priority, labels, and assignee +3. Creates the issue via `POST /rest/api/3/issue` +4. Returns the new issue key and ID + +--- + +### Search Issues (JQL) + +Searches for Jira issues using JQL. Returns structured results with key +fields extracted (summary, status, assignee, priority). + +```bash +one flow execute jira-search-issues.flow.json \ + --input jiraConnectionKey="" \ + --input jiraCloudId="" \ + --input jql="project = ENG AND status = 'In Progress' ORDER BY updated DESC" \ + --input maxResults=10 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `jiraConnectionKey` | Yes | Your Jira connection key | +| `jiraCloudId` | Yes | Jira Cloud site ID (UUID) | +| `jql` | Yes | JQL query (must be bounded) | +| `maxResults` | No | Max issues to return (1-100, default 20) | +| `fields` | No | Comma-separated field list (default: summary, status, assignee, priority, created, updated) | +| `nextPageToken` | No | Pagination token from a previous response | + +**JQL examples:** + +| Query | Meaning | +|-------|---------| +| `project = ENG AND status = "To Do"` | Open issues in ENG project | +| `assignee = currentUser() ORDER BY updated DESC` | Your issues, recently updated first | +| `priority = High AND statusCategory != Done` | High priority, not yet done | +| `labels = bug AND created >= -7d` | Bugs created in the last 7 days | +| `text ~ "rate limit"` | Full-text search | + +--- + +### Get Issue + +Retrieves full details for a Jira issue. Extracts and structures key fields +including description (ADF decoded to plain text), recent comments, status, +and assignee. + +```bash +one flow execute jira-get-issue.flow.json \ + --input jiraConnectionKey="" \ + --input jiraCloudId="" \ + --input issueIdOrKey="ENG-42" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `jiraConnectionKey` | Yes | Your Jira connection key | +| `jiraCloudId` | Yes | Jira Cloud site ID (UUID) | +| `issueIdOrKey` | Yes | Issue ID or key (e.g., `ENG-42`) | +| `fields` | No | Comma-separated field list (default: `*navigable`) | +| `expand` | No | Expand options: `renderedFields`, `transitions`, `changelog`, `editmeta` | + +**What it does under the hood:** + +1. Fetches the issue via `GET /rest/api/3/issue/{issueIdOrKey}` +2. Decodes ADF description to plain text +3. Extracts last 5 comments with author and timestamp +4. Returns structured response with key fields + +--- + +### Transition Issue + +Moves a Jira issue through its workflow. Automatically looks up available +transitions and matches by status name, so you don't need to know transition +IDs. + +```bash +one flow execute jira-transition-issue.flow.json \ + --input jiraConnectionKey="" \ + --input jiraCloudId="" \ + --input issueIdOrKey="ENG-42" \ + --input targetStatus="Done" \ + --input comment="Fixed in PR #187" \ + --input resolution="Fixed" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `jiraConnectionKey` | Yes | Your Jira connection key | +| `jiraCloudId` | Yes | Jira Cloud site ID (UUID) | +| `issueIdOrKey` | Yes | Issue ID or key | +| `targetStatus` | Yes | Target status name (case-insensitive match) | +| `comment` | No | Comment to add during transition | +| `resolution` | No | Resolution name (e.g., `Fixed`, `Won't Do`) | + +**What it does under the hood:** + +1. Fetches available transitions via `GET /rest/api/3/issue/{id}/transitions` +2. Matches `targetStatus` against transition names and destination statuses (case-insensitive) +3. Builds the transition payload with optional comment (ADF) and resolution +4. Executes the transition via `POST /rest/api/3/issue/{id}/transitions` +5. Throws a descriptive error listing available transitions if no match is found + +--- + +### Add Comment + +Adds a comment to a Jira issue. Converts plain text to Atlassian Document +Format automatically. Supports visibility restrictions. + +```bash +one flow execute jira-add-comment.flow.json \ + --input jiraConnectionKey="" \ + --input jiraCloudId="" \ + --input issueIdOrKey="ENG-42" \ + --input comment="Investigated the root cause. See attached logs." +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `jiraConnectionKey` | Yes | Your Jira connection key | +| `jiraCloudId` | Yes | Jira Cloud site ID (UUID) | +| `issueIdOrKey` | Yes | Issue ID or key | +| `comment` | Yes | Comment text (plain text, auto-converted to ADF) | +| `visibilityType` | No | `group` or `role` | +| `visibilityValue` | No | Group name or role name to restrict visibility | + +**What it does under the hood:** + +1. Converts plain text to ADF with paragraph breaks preserved +2. Optionally attaches visibility restrictions (group or role) +3. Posts the comment via `POST /rest/api/3/issue/{id}/comment` + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Bug triage pipeline**: Chain `jira-search-issues` to find open bugs, then `jira-transition-issue` to move them to "In Review". +- **Sprint standup report**: Use `jira-search-issues` with `assignee = currentUser() AND sprint in openSprints()` and pipe to Slack. +- **Auto-create from alerts**: Use `jira-create-issue` as a step in a multi-platform flow triggered by PagerDuty or Datadog. +- **Close with comment**: Chain `jira-add-comment` and `jira-transition-issue` to close issues with a summary. +- **Issue detail lookup**: Use `jira-get-issue` with `expand=transitions` to discover available workflow transitions before building automation. + +## Key Concepts + +**Atlassian Document Format (ADF):** Jira's rich text format. These flows handle the conversion from plain text to ADF automatically. If you need richer formatting (tables, code blocks, mentions), modify the `textToAdf` function in the code steps. + +**JQL (Jira Query Language):** Jira's query language for searching issues. Queries must be "bounded" (include a search restriction like `project = X`). The enhanced search endpoint used here supports up to 5000 results per query. + +**Transitions:** Jira issues move through workflow states via transitions. The transition ID (not the status name) is what the API requires. The `jira-transition-issue` flow handles this lookup automatically. + +The orchestration knowledge is in the flow's `code` steps. Read them to understand the ADF encoding, JQL parameter construction, and transition matching patterns, then build your own variations. diff --git a/flows/jira/jira-add-comment.flow.json b/flows/jira/jira-add-comment.flow.json new file mode 100644 index 0000000..229d3d7 --- /dev/null +++ b/flows/jira/jira-add-comment.flow.json @@ -0,0 +1,72 @@ +{ + "key": "jira-add-comment", + "name": "Add a Comment to a Jira Issue", + "description": "Adds a comment to a Jira issue. Converts plain text to Atlassian Document Format (ADF) automatically. Supports optional visibility restrictions (group or role).", + "version": "1", + "inputs": { + "jiraConnectionKey": { + "type": "string", + "required": true, + "description": "Jira connection key", + "connection": { "platform": "jira" } + }, + "jiraCloudId": { + "type": "string", + "required": true, + "description": "Jira Cloud site ID (UUID). Get it via the Get Sites action." + }, + "issueIdOrKey": { + "type": "string", + "required": true, + "description": "Issue ID or key (e.g., 'PROJ-123')" + }, + "comment": { + "type": "string", + "required": true, + "description": "Comment text (plain text, auto-converted to ADF)" + }, + "visibilityType": { + "type": "string", + "required": false, + "description": "Restrict visibility: 'group' or 'role'" + }, + "visibilityValue": { + "type": "string", + "required": false, + "description": "Group name or role name to restrict visibility to" + } + }, + "steps": [ + { + "id": "buildComment", + "name": "Build ADF comment payload", + "type": "code", + "code": { + "source": "const { comment, visibilityType, visibilityValue } = $.input;\n\nif (!comment) throw new Error('comment text is required');\n\n// Convert plain text to ADF, preserving line breaks as separate paragraphs\nconst paragraphs = comment.split('\\n');\nconst adfContent = paragraphs.map(line => ({\n type: 'paragraph',\n content: line.trim() ? [{ type: 'text', text: line }] : []\n}));\n\nconst payload = {\n body: {\n type: 'doc',\n version: 1,\n content: adfContent\n }\n};\n\nif (visibilityType && visibilityValue) {\n payload.visibility = {\n type: visibilityType,\n value: visibilityValue,\n identifier: visibilityValue\n };\n}\n\nreturn payload;" + } + }, + { + "id": "addComment", + "name": "Post the comment to the issue", + "type": "action", + "action": { + "platform": "jira", + "actionId": "conn_mod_def::GJ4qWiVfkSM::sbJJ-I2yQjGHyJq0D_evxA", + "connectionKey": "$.input.jiraConnectionKey", + "pathVars": { + "jiraCloudId": "$.input.jiraCloudId", + "issueIdOrKey": "$.input.issueIdOrKey" + }, + "data": "$.steps.buildComment.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.addComment.response || {};\nconst issueKey = $.input.issueIdOrKey;\n\nreturn {\n commentId: resp.id || '',\n issueKey,\n author: resp.author ? resp.author.displayName : '',\n created: resp.created || '',\n visibility: resp.visibility || null,\n success: !!resp.id,\n summary: resp.id\n ? `Added comment ${resp.id} to ${issueKey}`\n : `Failed to add comment to ${issueKey}`\n};" + } + } + ] +} diff --git a/flows/jira/jira-create-issue.flow.json b/flows/jira/jira-create-issue.flow.json new file mode 100644 index 0000000..cf3586d --- /dev/null +++ b/flows/jira/jira-create-issue.flow.json @@ -0,0 +1,91 @@ +{ + "key": "jira-create-issue", + "name": "Create a Jira Issue", + "description": "Creates a Jira Cloud issue with proper Atlassian Document Format (ADF) for the description field. Handles project/issue-type lookup by key, ADF encoding, labels, priority, and optional subtask creation.", + "version": "1", + "inputs": { + "jiraConnectionKey": { + "type": "string", + "required": true, + "description": "Jira connection key", + "connection": { "platform": "jira" } + }, + "jiraCloudId": { + "type": "string", + "required": true, + "description": "Jira Cloud site ID (UUID). Get it via the Get Sites action." + }, + "projectKey": { + "type": "string", + "required": true, + "description": "Project key (e.g., 'ENG', 'PROJ')" + }, + "issueTypeId": { + "type": "string", + "required": true, + "description": "Issue type ID (e.g., '10001' for Task). Use Get Create Issue Metadata to find valid IDs." + }, + "summary": { + "type": "string", + "required": true, + "description": "Issue summary (title)" + }, + "description": { + "type": "string", + "required": false, + "description": "Plain text description. Auto-converted to Atlassian Document Format (ADF)." + }, + "priority": { + "type": "string", + "required": false, + "description": "Priority name (e.g., 'High', 'Medium', 'Low')" + }, + "labels": { + "type": "array", + "required": false, + "description": "Array of label strings to apply" + }, + "assigneeAccountId": { + "type": "string", + "required": false, + "description": "Account ID of the assignee" + }, + "parentKey": { + "type": "string", + "required": false, + "description": "Parent issue key for subtask creation (e.g., 'PROJ-123')" + } + }, + "steps": [ + { + "id": "buildPayload", + "name": "Build issue creation payload with ADF description", + "type": "code", + "code": { + "source": "const { projectKey, issueTypeId, summary, description, priority, labels, assigneeAccountId, parentKey } = $.input;\n\nif (!projectKey || !issueTypeId || !summary) throw new Error('projectKey, issueTypeId, and summary are required');\n\n// Convert plain text to Atlassian Document Format (ADF)\nfunction textToAdf(text) {\n if (!text) return undefined;\n const paragraphs = text.split('\\n').filter(line => line.trim() !== '');\n return {\n type: 'doc',\n version: 1,\n content: paragraphs.map(p => ({\n type: 'paragraph',\n content: [{ type: 'text', text: p }]\n }))\n };\n}\n\nconst fields = {\n project: { key: projectKey },\n issuetype: { id: issueTypeId },\n summary: summary\n};\n\nif (description) fields.description = textToAdf(description);\nif (priority) fields.priority = { name: priority };\nif (labels && labels.length > 0) fields.labels = labels;\nif (assigneeAccountId) fields.assignee = { accountId: assigneeAccountId };\nif (parentKey) fields.parent = { key: parentKey };\n\nreturn { fields };" + } + }, + { + "id": "createIssue", + "name": "Create the issue via Jira API", + "type": "action", + "action": { + "platform": "jira", + "actionId": "conn_mod_def::GJ4qeeOHWcA::Sdqh-9IBQV-VrJ3JHPYrpA", + "connectionKey": "$.input.jiraConnectionKey", + "pathVars": { + "jiraCloudId": "$.input.jiraCloudId" + }, + "data": "$.steps.buildPayload.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createIssue.response || {};\nconst projectKey = $.input.projectKey;\n\nreturn {\n id: resp.id || '',\n key: resp.key || '',\n self: resp.self || '',\n projectKey: projectKey,\n summary: $.input.summary,\n created: !!resp.id,\n summary_text: resp.id\n ? `Created ${resp.key}: \"${$.input.summary}\" in project ${projectKey}`\n : 'Failed to create issue'\n};" + } + } + ] +} diff --git a/flows/jira/jira-get-issue.flow.json b/flows/jira/jira-get-issue.flow.json new file mode 100644 index 0000000..45c86c6 --- /dev/null +++ b/flows/jira/jira-get-issue.flow.json @@ -0,0 +1,68 @@ +{ + "key": "jira-get-issue", + "name": "Get a Jira Issue", + "description": "Retrieves full details for a Jira issue by ID or key. Returns a clean, structured response with key fields extracted (summary, status, assignee, priority, description, comments, etc.).", + "version": "1", + "inputs": { + "jiraConnectionKey": { + "type": "string", + "required": true, + "description": "Jira connection key", + "connection": { "platform": "jira" } + }, + "jiraCloudId": { + "type": "string", + "required": true, + "description": "Jira Cloud site ID (UUID). Get it via the Get Sites action." + }, + "issueIdOrKey": { + "type": "string", + "required": true, + "description": "Issue ID or key (e.g., 'PROJ-123' or '10001')" + }, + "fields": { + "type": "string", + "required": false, + "default": "*navigable", + "description": "Comma-separated field list. '*all' for all, '*navigable' for navigable fields. Prefix with '-' to exclude." + }, + "expand": { + "type": "string", + "required": false, + "description": "Comma-separated expand options: renderedFields, names, transitions, changelog, editmeta" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const params = {};\nconst fields = $.input.fields || '*navigable';\nparams.fields = fields;\nif ($.input.expand) params.expand = $.input.expand;\nreturn { params };" + } + }, + { + "id": "getIssue", + "name": "Fetch issue details from Jira", + "type": "action", + "action": { + "platform": "jira", + "actionId": "conn_mod_def::GJ4qe6eIMMg::dTjGYNwLTBu34ueB7q45TA", + "connectionKey": "$.input.jiraConnectionKey", + "pathVars": { + "jiraCloudId": "$.input.jiraCloudId", + "issueIdOrKey": "$.input.issueIdOrKey" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Extract and format key issue fields", + "type": "code", + "code": { + "source": "const resp = $.steps.getIssue.response || {};\nconst f = resp.fields || {};\n\n// Extract plain text from ADF body\nfunction adfToText(adf) {\n if (!adf || !adf.content) return '';\n return adf.content.map(block => {\n if (block.content) return block.content.map(node => node.text || '').join('');\n return '';\n }).join('\\n');\n}\n\nconst description = typeof f.description === 'string'\n ? f.description\n : adfToText(f.description);\n\nconst comments = (f.comment && f.comment.comments || []).slice(-5).map(c => ({\n id: c.id,\n author: c.author ? c.author.displayName : '',\n body: typeof c.body === 'string' ? c.body : adfToText(c.body),\n created: c.created\n}));\n\nreturn {\n id: resp.id || '',\n key: resp.key || '',\n self: resp.self || '',\n summary: f.summary || '',\n status: f.status ? f.status.name : '',\n statusCategory: f.status && f.status.statusCategory ? f.status.statusCategory.name : '',\n assignee: f.assignee ? f.assignee.displayName : 'Unassigned',\n assigneeAccountId: f.assignee ? f.assignee.accountId : null,\n reporter: f.reporter ? f.reporter.displayName : '',\n priority: f.priority ? f.priority.name : '',\n issueType: f.issuetype ? f.issuetype.name : '',\n project: f.project ? { key: f.project.key, name: f.project.name } : {},\n description: description.length > 2000 ? description.substring(0, 2000) + '...' : description,\n labels: f.labels || [],\n created: f.created || '',\n updated: f.updated || '',\n resolution: f.resolution ? f.resolution.name : null,\n recentComments: comments,\n transitions: resp.transitions || [],\n summary_text: `${resp.key}: \"${f.summary || ''}\" [${f.status ? f.status.name : 'unknown'}] assigned to ${f.assignee ? f.assignee.displayName : 'nobody'}`\n};" + } + } + ] +} diff --git a/flows/jira/jira-search-issues.flow.json b/flows/jira/jira-search-issues.flow.json new file mode 100644 index 0000000..fcad1ca --- /dev/null +++ b/flows/jira/jira-search-issues.flow.json @@ -0,0 +1,73 @@ +{ + "key": "jira-search-issues", + "name": "Search Jira Issues via JQL", + "description": "Searches for Jira issues using JQL (Jira Query Language). Returns issue keys, summaries, statuses, assignees, and other fields. Handles pagination automatically.", + "version": "1", + "inputs": { + "jiraConnectionKey": { + "type": "string", + "required": true, + "description": "Jira connection key", + "connection": { "platform": "jira" } + }, + "jiraCloudId": { + "type": "string", + "required": true, + "description": "Jira Cloud site ID (UUID). Get it via the Get Sites action." + }, + "jql": { + "type": "string", + "required": true, + "description": "JQL query string. Must be bounded (include a search restriction). Examples: 'project = ENG AND status = Open', 'assignee = currentUser() ORDER BY updated DESC'" + }, + "maxResults": { + "type": "number", + "required": false, + "default": 20, + "description": "Max issues to return (1-100, default 20)" + }, + "fields": { + "type": "string", + "required": false, + "default": "summary,status,assignee,priority,created,updated", + "description": "Comma-separated field list. Use '*all' for all fields, '*navigable' for navigable fields." + }, + "nextPageToken": { + "type": "string", + "required": false, + "description": "Pagination token from a previous response" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { jql, maxResults, fields, nextPageToken } = $.input;\n\nif (!jql) throw new Error('jql query is required');\n\nconst max = Math.max(1, Math.min(100, maxResults || 20));\nconst fieldList = fields || 'summary,status,assignee,priority,created,updated';\n\nconst params = {\n jql: jql,\n maxResults: String(max),\n fields: fieldList\n};\n\nif (nextPageToken) params.nextPageToken = nextPageToken;\n\nreturn { params, jql, max };" + } + }, + { + "id": "searchIssues", + "name": "Search issues via JQL", + "type": "action", + "action": { + "platform": "jira", + "actionId": "conn_mod_def::GJ4qbdtRYhI::S3uWGiHPRMCzy6qMTxFe1A", + "connectionKey": "$.input.jiraConnectionKey", + "pathVars": { + "jiraCloudId": "$.input.jiraCloudId" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Extract and format issue results", + "type": "code", + "code": { + "source": "const resp = $.steps.searchIssues.response || {};\nconst issues = (resp.issues || []).map(issue => {\n const f = issue.fields || {};\n return {\n id: issue.id,\n key: issue.key,\n summary: f.summary || '',\n status: f.status ? f.status.name : '',\n statusCategory: f.status && f.status.statusCategory ? f.status.statusCategory.name : '',\n assignee: f.assignee ? f.assignee.displayName : 'Unassigned',\n assigneeAccountId: f.assignee ? f.assignee.accountId : null,\n priority: f.priority ? f.priority.name : '',\n created: f.created || '',\n updated: f.updated || ''\n };\n});\n\nconst result = {\n issues,\n totalFound: issues.length,\n isLast: resp.isLast !== false,\n jql: $.steps.buildParams.output.jql,\n summary: `Found ${issues.length} issue${issues.length === 1 ? '' : 's'} matching JQL query`\n};\n\nif (resp.nextPageToken) result.nextPageToken = resp.nextPageToken;\n\nreturn result;" + } + } + ] +} diff --git a/flows/jira/jira-transition-issue.flow.json b/flows/jira/jira-transition-issue.flow.json new file mode 100644 index 0000000..2013f91 --- /dev/null +++ b/flows/jira/jira-transition-issue.flow.json @@ -0,0 +1,86 @@ +{ + "key": "jira-transition-issue", + "name": "Transition a Jira Issue", + "description": "Moves a Jira issue through its workflow. First fetches available transitions for the issue, matches the target status by name, then performs the transition. Optionally adds a comment and sets a resolution.", + "version": "1", + "inputs": { + "jiraConnectionKey": { + "type": "string", + "required": true, + "description": "Jira connection key", + "connection": { "platform": "jira" } + }, + "jiraCloudId": { + "type": "string", + "required": true, + "description": "Jira Cloud site ID (UUID). Get it via the Get Sites action." + }, + "issueIdOrKey": { + "type": "string", + "required": true, + "description": "Issue ID or key (e.g., 'PROJ-123' or '10001')" + }, + "targetStatus": { + "type": "string", + "required": true, + "description": "Target status name to transition to (e.g., 'In Progress', 'Done', 'To Do'). Case-insensitive match." + }, + "comment": { + "type": "string", + "required": false, + "description": "Optional comment to add during the transition" + }, + "resolution": { + "type": "string", + "required": false, + "description": "Resolution name (e.g., 'Fixed', 'Won\\'t Do') -- only for transitions to Done-category statuses" + } + }, + "steps": [ + { + "id": "getTransitions", + "name": "Fetch available transitions for the issue", + "type": "action", + "action": { + "platform": "jira", + "actionId": "conn_mod_def::GJ4qe5JzXH4::-j_7Sd24TPuHjYW4JW_MuQ", + "connectionKey": "$.input.jiraConnectionKey", + "pathVars": { + "jiraCloudId": "$.input.jiraCloudId", + "issueIdOrKey": "$.input.issueIdOrKey" + } + } + }, + { + "id": "findTransition", + "name": "Match target status to an available transition", + "type": "code", + "code": { + "source": "const transitions = ($.steps.getTransitions.response || {}).transitions || [];\nconst target = $.input.targetStatus.toLowerCase();\n\nconst match = transitions.find(t => t.name.toLowerCase() === target || (t.to && t.to.name && t.to.name.toLowerCase() === target));\n\nif (!match) {\n const available = transitions.map(t => `${t.name} (id: ${t.id}, to: ${t.to ? t.to.name : 'unknown'})`).join(', ');\n throw new Error(`No transition found for target status \"${$.input.targetStatus}\". Available transitions: ${available || 'none'}`);\n}\n\n// Build transition payload\nconst payload = { transition: { id: match.id } };\n\nif ($.input.comment) {\n payload.update = {\n comment: [{\n add: {\n body: {\n type: 'doc',\n version: 1,\n content: [{\n type: 'paragraph',\n content: [{ type: 'text', text: $.input.comment }]\n }]\n }\n }\n }]\n };\n}\n\nif ($.input.resolution) {\n payload.fields = payload.fields || {};\n payload.fields.resolution = { name: $.input.resolution };\n}\n\nreturn { payload, transitionId: match.id, transitionName: match.name, targetStatusName: match.to ? match.to.name : match.name };" + } + }, + { + "id": "performTransition", + "name": "Execute the workflow transition", + "type": "action", + "action": { + "platform": "jira", + "actionId": "conn_mod_def::GJ4qfCdCUXo::EHXg3vKkR9eBZqXkKHkbZg", + "connectionKey": "$.input.jiraConnectionKey", + "pathVars": { + "jiraCloudId": "$.input.jiraCloudId", + "issueIdOrKey": "$.input.issueIdOrKey" + }, + "data": "$.steps.findTransition.output.payload" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const transitionName = $.steps.findTransition.output.transitionName;\nconst targetStatus = $.steps.findTransition.output.targetStatusName;\nconst issueKey = $.input.issueIdOrKey;\n\nreturn {\n issueKey,\n transitionId: $.steps.findTransition.output.transitionId,\n transitionName,\n newStatus: targetStatus,\n hadComment: !!$.input.comment,\n hadResolution: !!$.input.resolution,\n success: true,\n summary: `Transitioned ${issueKey} via \"${transitionName}\" to \"${targetStatus}\"${$.input.comment ? ' with comment' : ''}${$.input.resolution ? ' (resolution: ' + $.input.resolution + ')' : ''}`\n};" + } + } + ] +} diff --git a/flows/meet-geek/README.md b/flows/meet-geek/README.md new file mode 100644 index 0000000..3f3d02c --- /dev/null +++ b/flows/meet-geek/README.md @@ -0,0 +1,61 @@ +--- +name: meet-geek +description: | + MeetGeek meeting transcript flow for the One CLI. Retrieves meeting transcripts + with speaker labels and timestamps. +triggers: + - "meeting transcript" + - "meetgeek" + - "meet-geek" + - "/meet-geek" +--- + +# MeetGeek Flows + +Ready-to-run meeting transcript retrieval via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add meet-geek # Connect your MeetGeek account +one --agent list # Find your connection key +``` + +## Discovery + +You need a `meetingId` to retrieve a transcript. Find it with: + +```bash +# List recent meetings +one --agent actions search meet-geek "list meetings" +one --agent actions execute meet-geek +``` + +## Flows + +### Get Meeting Transcript + +Retrieves the full transcript for a meeting with speaker labels and timestamps. +Supports pagination for long meetings. + +```bash +one flow execute meet-geek-get-transcript.flow.json \ + --input meetGeekConnectionKey="" \ + --input meetingId="17c36737-1bf5-4626-8483-88285f6a33ee" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `meetGeekConnectionKey` | Yes | Your MeetGeek connection key | +| `meetingId` | Yes | The meeting ID to get the transcript for | +| `limit` | No | Sentences per page (default: 100) | + +**What it does under the hood:** + +1. Calls MeetGeek API (`GET /v1/meetings/{meetingId}/transcript`) with pagination params +2. Parses transcript sentences with speaker labels, timestamps, and text +3. Identifies unique speakers +4. Returns structured transcript with pagination cursors for continuation diff --git a/flows/meet-geek/meet-geek-get-transcript.flow.json b/flows/meet-geek/meet-geek-get-transcript.flow.json new file mode 100644 index 0000000..b571ba2 --- /dev/null +++ b/flows/meet-geek/meet-geek-get-transcript.flow.json @@ -0,0 +1,51 @@ +{ + "key": "meet-geek-get-transcript", + "name": "Get Meeting Transcript from MeetGeek", + "description": "Retrieve a meeting transcript with speaker labels and timestamps from MeetGeek. Handles pagination to fetch complete transcripts.", + "version": "1", + "inputs": { + "meetGeekConnectionKey": { + "type": "string", + "required": true, + "description": "MeetGeek connection key", + "connection": { "platform": "meet-geek" } + }, + "meetingId": { + "type": "string", + "required": true, + "description": "The meeting ID to retrieve the transcript for" + }, + "limit": { + "type": "number", + "required": false, + "default": 100, + "description": "Number of transcript sentences to fetch per page" + } + }, + "steps": [ + { + "id": "fetchTranscript", + "name": "Fetch meeting transcript", + "type": "action", + "action": { + "platform": "meet-geek", + "actionId": "conn_mod_def::GJ46X7dh6eg::2Ax7v7w5T3qlWbXFI2CF2w", + "connectionKey": "$.input.meetGeekConnectionKey", + "pathVars": { + "meetingId": "$.input.meetingId" + }, + "queryParams": { + "limit": "$.input.limit" + } + } + }, + { + "id": "formatResult", + "name": "Format transcript output", + "type": "code", + "code": { + "source": "const resp = $.steps.fetchTranscript.response || {};\nconst sentences = resp.sentences || [];\nconst pagination = resp.pagination || {};\n\nconst speakers = [...new Set(sentences.map(s => s.speaker).filter(Boolean))];\n\nconst transcript = sentences.map(s => ({\n speaker: s.speaker || 'Unknown',\n timestamp: s.timestamp || '',\n text: s.transcript || ''\n}));\n\nreturn {\n meetingId: resp.meeting_id || $.input.meetingId,\n sentences: transcript,\n totalSentences: transcript.length,\n speakers,\n nextCursor: pagination.next_cursor || null,\n previousCursor: pagination.previous_cursor || null,\n summary: `Retrieved ${transcript.length} transcript sentence${transcript.length === 1 ? '' : 's'} from ${speakers.length} speaker${speakers.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/netlify/README.md b/flows/netlify/README.md new file mode 100644 index 0000000..ecb2383 --- /dev/null +++ b/flows/netlify/README.md @@ -0,0 +1,80 @@ +--- +name: netlify +description: | + Netlify deployment and site management flows for the One CLI. Deploy sites, + list sites, and manage deploy previews. +triggers: + - "deploy netlify" + - "netlify deploy" + - "netlify sites" + - "list sites" + - "/netlify" +--- + +# Netlify Flows + +Ready-to-run site deployment and management via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add netlify # Connect your Netlify account +one --agent list # Find your connection key +``` + +## Discovery + +Deploying requires a `siteId`. Find it using the list sites flow: + +```bash +# List all sites (returns site IDs, names, and URLs) +one flow execute netlify-list-sites.flow.json \ + --input netlifyConnectionKey="" +``` + +## Flows + +### Deploy Site + +Triggers a new deploy for a Netlify site. Supports production deploys, branch +targeting, and deploy titles. + +```bash +one flow execute netlify-deploy-site.flow.json \ + --input netlifyConnectionKey="" \ + --input siteId="12345678-90ab-cdef-1234-567890abcdef" \ + --input production=true \ + --input branch="main" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `netlifyConnectionKey` | Yes | Your Netlify connection key | +| `siteId` | Yes | Netlify site ID to deploy | +| `production` | No | Set `true` for production deploy | +| `branch` | No | Branch to deploy from | +| `title` | No | Title for this deploy | + +### List Sites + +Lists all sites in your Netlify account with IDs, names, URLs, and status. + +```bash +one flow execute netlify-list-sites.flow.json \ + --input netlifyConnectionKey="" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `netlifyConnectionKey` | Yes | Your Netlify connection key | + +## Adapting These Flows + +- **Deploy preview**: Set `production=false` and specify a feature branch +- **Auto-deploy pipeline**: Chain with a GitHub action or webhook trigger +- **Deploy + notify**: Follow up with a Slack flow to announce the deploy diff --git a/flows/netlify/netlify-deploy-site.flow.json b/flows/netlify/netlify-deploy-site.flow.json new file mode 100644 index 0000000..1f12b36 --- /dev/null +++ b/flows/netlify/netlify-deploy-site.flow.json @@ -0,0 +1,67 @@ +{ + "key": "netlify-deploy-site", + "name": "Deploy a Netlify Site", + "description": "Trigger a new deploy for a Netlify site. Supports production deploys, deploy previews, branch targeting, and custom deploy titles.", + "version": "1", + "inputs": { + "netlifyConnectionKey": { + "type": "string", + "required": true, + "description": "Netlify connection key", + "connection": { "platform": "netlify" } + }, + "siteId": { + "type": "string", + "required": true, + "description": "Netlify site ID to deploy" + }, + "production": { + "type": "boolean", + "required": false, + "default": false, + "description": "Set true for a production deploy" + }, + "branch": { + "type": "string", + "required": false, + "description": "Branch to deploy from" + }, + "title": { + "type": "string", + "required": false, + "description": "Title for this deploy" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build deploy query params", + "type": "code", + "code": { + "source": "const { production, branch, title } = $.input;\nconst params = {};\nif (production) params.production = 'true';\nif (branch) params.branch = branch;\nif (title) params.title = title;\nreturn { params };" + } + }, + { + "id": "createDeploy", + "name": "Create site deploy", + "type": "action", + "action": { + "platform": "netlify", + "actionId": "conn_mod_def::GJ5BNKCPGL0::BleJube2SSi0hNuBEglgVQ", + "connectionKey": "$.input.netlifyConnectionKey", + "pathVars": { + "siteId": "$.input.siteId" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Format deploy result", + "type": "code", + "code": { + "source": "const resp = $.steps.createDeploy.response || {};\nreturn {\n deployId: resp.id || '',\n siteId: resp.site_id || '',\n state: resp.state || '',\n url: resp.ssl_url || resp.url || '',\n deployUrl: resp.deploy_ssl_url || resp.deploy_url || '',\n adminUrl: resp.admin_url || '',\n branch: resp.branch || '',\n context: resp.context || '',\n title: resp.title || '',\n createdAt: resp.created_at || '',\n summary: resp.id ? `Deploy ${resp.id} created (state: ${resp.state || 'unknown'}) for ${resp.ssl_url || resp.url || 'site'}` : 'Failed to create deploy'\n};" + } + } + ] +} diff --git a/flows/netlify/netlify-list-sites.flow.json b/flows/netlify/netlify-list-sites.flow.json new file mode 100644 index 0000000..a617c53 --- /dev/null +++ b/flows/netlify/netlify-list-sites.flow.json @@ -0,0 +1,34 @@ +{ + "key": "netlify-list-sites", + "name": "List Netlify Sites", + "description": "List all sites in your Netlify account. Returns site IDs, names, URLs, and deploy status.", + "version": "1", + "inputs": { + "netlifyConnectionKey": { + "type": "string", + "required": true, + "description": "Netlify connection key", + "connection": { "platform": "netlify" } + } + }, + "steps": [ + { + "id": "listSites", + "name": "List all Netlify sites", + "type": "action", + "action": { + "platform": "netlify", + "actionId": "conn_mod_def::GJ5BP3pIYwA::sqd9N8qzRMGEmGLiLpihQg", + "connectionKey": "$.input.netlifyConnectionKey" + } + }, + { + "id": "formatResult", + "name": "Format site list", + "type": "code", + "code": { + "source": "const resp = $.steps.listSites.response || [];\nconst sites = (Array.isArray(resp) ? resp : []).map(s => ({\n id: s.id || '',\n name: s.name || '',\n url: s.ssl_url || s.url || '',\n adminUrl: s.admin_url || '',\n state: s.state || '',\n createdAt: s.created_at || '',\n updatedAt: s.updated_at || ''\n}));\n\nreturn {\n sites,\n totalSites: sites.length,\n summary: `Found ${sites.length} Netlify site${sites.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/notion/README.md b/flows/notion/README.md new file mode 100644 index 0000000..3ad7dbf --- /dev/null +++ b/flows/notion/README.md @@ -0,0 +1,134 @@ +--- +name: notion +description: | + Notion integration flows for the One CLI. Create pages in databases or as + sub-pages, and query databases with filters, sorts, and pagination. +triggers: + - "notion" + - "create page notion" + - "query database notion" + - "notion database" + - "add to notion" + - "/notion" +--- + +# Notion Flows + +Ready-to-run workflows for Notion via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add notion # Connect your Notion workspace +one --agent list # Find your connection key +``` + +## Discovery + +You need a `databaseId` or `parentId` (page ID) before using the flows. Find them with: + +```bash +# Search for databases and pages by title +one --agent actions search notion "search" +one --agent actions execute notion \ + --body '{"query":"My Database","filter":{"property":"object","value":"database"}}' + +# Search for pages +one --agent actions execute notion \ + --body '{"query":"My Page","filter":{"property":"object","value":"page"}}' +``` + +Database and page IDs are also visible in Notion URLs: `notion.so/Your-Page-<32-char-id>`. + +## Flows + +### Create Page + +Create a new page in a Notion database (with properties) or as a child of another page. + +```bash +# Create in a database +one flow execute notion-create-page.flow.json \ + --input notionConnectionKey="" \ + --input parentType="database" \ + --input parentId="" \ + --input title="Weekly Report" \ + --input content="Summary of this week's progress..." + +# Create as a sub-page +one flow execute notion-create-page.flow.json \ + --input notionConnectionKey="" \ + --input parentType="page" \ + --input parentId="" \ + --input title="Meeting Notes" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `notionConnectionKey` | Yes | Notion connection key | +| `parentType` | Yes | 'database' or 'page' | +| `parentId` | Yes | Database ID or parent page ID | +| `title` | Yes | Page title | +| `properties` | No | Database properties object (for database parent) | +| `content` | No | Plain text content (added as a paragraph block) | +| `children` | No | Array of Notion block objects (overrides content) | + +**What it does under the hood:** + +1. Builds parent reference (database_id or page_id) +2. For database parents, maps the title into the Name/title property +3. Converts plain text content to a paragraph block +4. Creates the page via `POST /pages` + +**Database properties example:** + +```json +{ + "Status": {"select": {"name": "In Progress"}}, + "Priority": {"select": {"name": "High"}}, + "Due Date": {"date": {"start": "2024-03-20"}} +} +``` + +### Query Database + +Query a Notion database with filters, sorts, and pagination. + +```bash +one flow execute notion-query-database.flow.json \ + --input notionConnectionKey="" \ + --input databaseId="" \ + --input filter='{"property":"Status","select":{"equals":"In Progress"}}' \ + --input sorts='[{"property":"Created","direction":"descending"}]' \ + --input pageSize=10 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `notionConnectionKey` | Yes | Notion connection key | +| `databaseId` | Yes | Notion database ID | +| `filter` | No | Notion filter object | +| `sorts` | No | Array of sort objects | +| `pageSize` | No | Results per page (max 100, default 25) | +| `startCursor` | No | Pagination cursor | + +**Filter examples:** + +| Filter | Description | +|--------|-------------| +| `{"property": "Status", "select": {"equals": "Done"}}` | Status is Done | +| `{"property": "Name", "rich_text": {"contains": "report"}}` | Name contains "report" | +| `{"property": "Created", "date": {"after": "2024-01-01"}}` | Created after date | +| `{"and": [{...}, {...}]}` | Compound filter | + +## Adapting These Flows + +- **Task tracker**: Create pages in a tasks database from email or Slack triggers. +- **Meeting log**: Chain `calendly-list-events` into `notion-create-page` to log meetings. +- **Dashboard data**: Query a database and pipe results into Google Sheets. +- **Rich content**: Pass custom `children` blocks to create pages with headings, lists, toggles, and embedded content. diff --git a/flows/notion/notion-create-page.flow.json b/flows/notion/notion-create-page.flow.json new file mode 100644 index 0000000..5e82344 --- /dev/null +++ b/flows/notion/notion-create-page.flow.json @@ -0,0 +1,73 @@ +{ + "key": "notion-create-page", + "name": "Create a Page in Notion", + "description": "Create a new page in a Notion database or as a child of another page. Supports setting database properties and adding content blocks.", + "version": "1", + "inputs": { + "notionConnectionKey": { + "type": "string", + "required": true, + "description": "Notion connection key", + "connection": { "platform": "notion" } + }, + "parentType": { + "type": "string", + "required": true, + "description": "'database' to create a page in a database, or 'page' to create a sub-page" + }, + "parentId": { + "type": "string", + "required": true, + "description": "Database ID or parent page ID" + }, + "title": { + "type": "string", + "required": true, + "description": "Page title" + }, + "properties": { + "type": "object", + "required": false, + "description": "Database properties (for database parent). Example: {\"Status\": {\"select\": {\"name\": \"In Progress\"}}}" + }, + "content": { + "type": "string", + "required": false, + "description": "Plain text content to add as a paragraph block" + }, + "children": { + "type": "array", + "required": false, + "description": "Array of Notion block objects. Overrides 'content' if provided." + } + }, + "steps": [ + { + "id": "buildPage", + "name": "Build page payload", + "type": "code", + "code": { + "source": "const { parentType, parentId, title, properties, content, children } = $.input;\nif (!parentType || !parentId || !title) throw new Error('parentType, parentId, and title are required');\n\nconst body = {};\n\n// Set parent\nif (parentType === 'database') {\n body.parent = { database_id: parentId };\n // For database pages, title goes in properties\n body.properties = properties ? { ...properties } : {};\n // Find the title property or default to 'Name' or 'title'\n const titleProp = body.properties.Name ? 'Name' : (body.properties.title ? 'title' : 'Name');\n if (!body.properties[titleProp]) {\n body.properties[titleProp] = { title: [{ text: { content: title } }] };\n }\n} else {\n body.parent = { page_id: parentId };\n body.properties = { title: [{ text: { content: title } }] };\n}\n\n// Set children (content blocks)\nif (children && children.length > 0) {\n body.children = children;\n} else if (content) {\n body.children = [{\n object: 'block',\n type: 'paragraph',\n paragraph: {\n rich_text: [{ type: 'text', text: { content } }]\n }\n }];\n}\n\nreturn body;" + } + }, + { + "id": "createPage", + "name": "Create page via Notion API", + "type": "action", + "action": { + "platform": "notion", + "actionId": "conn_mod_def::GJ5EnlW1AEk::mEAQjNttT1GjVqNeMTvDiw", + "connectionKey": "$.input.notionConnectionKey", + "data": "$.steps.buildPage.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.createPage.response || {};\nconst url = resp.url || '';\nreturn {\n pageId: resp.id || '',\n url,\n parentType: $.input.parentType,\n title: $.input.title,\n created: !!resp.id,\n createdTime: resp.created_time || '',\n summary: resp.id\n ? `Created page \"${$.input.title}\" in Notion`\n : 'Failed to create page'\n};" + } + } + ] +} diff --git a/flows/notion/notion-query-database.flow.json b/flows/notion/notion-query-database.flow.json new file mode 100644 index 0000000..b771fe6 --- /dev/null +++ b/flows/notion/notion-query-database.flow.json @@ -0,0 +1,72 @@ +{ + "key": "notion-query-database", + "name": "Query a Notion Database", + "description": "Query a Notion database with filters, sorts, and pagination. Returns pages matching the filter criteria with their properties.", + "version": "1", + "inputs": { + "notionConnectionKey": { + "type": "string", + "required": true, + "description": "Notion connection key", + "connection": { "platform": "notion" } + }, + "databaseId": { + "type": "string", + "required": true, + "description": "Notion database ID" + }, + "filter": { + "type": "object", + "required": false, + "description": "Notion filter object. Example: {\"property\": \"Status\", \"select\": {\"equals\": \"Done\"}}" + }, + "sorts": { + "type": "array", + "required": false, + "description": "Array of sort objects. Example: [{\"property\": \"Created\", \"direction\": \"descending\"}]" + }, + "pageSize": { + "type": "number", + "required": false, + "default": 25, + "description": "Number of results per page (max 100)" + }, + "startCursor": { + "type": "string", + "required": false, + "description": "Pagination cursor from a previous response" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build database query", + "type": "code", + "code": { + "source": "const { filter, sorts, pageSize, startCursor } = $.input;\nconst body = { page_size: Math.min(pageSize || 25, 100) };\nif (filter) body.filter = filter;\nif (sorts && sorts.length > 0) body.sorts = sorts;\nif (startCursor) body.start_cursor = startCursor;\nreturn body;" + } + }, + { + "id": "queryDatabase", + "name": "Query database via Notion API", + "type": "action", + "action": { + "platform": "notion", + "actionId": "conn_mod_def::GJ5EnL2xERY::ib0N5v41TveieFZQUV-vuQ", + "connectionKey": "$.input.notionConnectionKey", + "pathVars": { + "dataSourceId": "$.input.databaseId" + }, + "data": "$.steps.buildQuery.output" + } + }, + { + "id": "formatResult", + "name": "Format the response", + "type": "code", + "code": { + "source": "const resp = $.steps.queryDatabase.response || {};\nconst results = resp.results || [];\n\nfunction extractTitle(props) {\n for (const key of Object.keys(props)) {\n const prop = props[key];\n if (prop.type === 'title' && prop.title && prop.title.length > 0) {\n return prop.title.map(t => t.plain_text || '').join('');\n }\n }\n return '';\n}\n\nreturn {\n pages: results.map(p => ({\n id: p.id,\n title: extractTitle(p.properties || {}),\n url: p.url || '',\n properties: p.properties || {},\n createdTime: p.created_time,\n lastEditedTime: p.last_edited_time\n })),\n count: results.length,\n hasMore: resp.has_more || false,\n nextCursor: resp.next_cursor || null,\n summary: `Retrieved ${results.length} page${results.length === 1 ? '' : 's'} from database`\n};" + } + } + ] +} diff --git a/flows/personal-ai/README.md b/flows/personal-ai/README.md new file mode 100644 index 0000000..e5965e1 --- /dev/null +++ b/flows/personal-ai/README.md @@ -0,0 +1,55 @@ +--- +name: personal-ai +description: | + Personal AI messaging flow for the One CLI. Send messages to a Personal AI + persona and get AI-generated responses with session continuity. +triggers: + - "personal ai" + - "personal-ai" + - "ai message" + - "/personal-ai" +--- + +# Personal AI Flows + +Ready-to-run AI messaging via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add personal-ai # Connect your Personal AI account +one --agent list # Find your connection key +``` + +## Flows + +### Send Message + +Send a message to a Personal AI persona and receive an AI-generated response. +Supports session continuity, context injection, and memory stacking. + +```bash +one flow execute personal-ai-send-message.flow.json \ + --input personalAiConnectionKey="" \ + --input text="What is an SLM?" \ + --input domainName="product-demo-jebzrhw" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `personalAiConnectionKey` | Yes | Your Personal AI connection key | +| `text` | Yes | Message to send to the AI | +| `domainName` | Yes | AI persona domain name (hyphenated text under the AI's name) | +| `context` | No | Additional context (e.g., "Reply in one sentence") | +| `sessionId` | No | Session ID to continue a conversation | +| `userName` | No | Name of the user sending the message | +| `isStack` | No | Set `true` to also add the message to the AI's memory | + +**What it does under the hood:** + +1. Builds the message payload with text, domain name, and optional settings +2. Sends via Personal AI API (`POST /v1/message`) +3. Returns AI response with confidence score and session ID for continuation diff --git a/flows/personal-ai/personal-ai-send-message.flow.json b/flows/personal-ai/personal-ai-send-message.flow.json new file mode 100644 index 0000000..b609cd1 --- /dev/null +++ b/flows/personal-ai/personal-ai-send-message.flow.json @@ -0,0 +1,74 @@ +{ + "key": "personal-ai-send-message", + "name": "Send Message to Personal AI", + "description": "Send a message to a Personal AI persona and get an AI-generated response. Supports session continuity, context injection, document references, and memory stacking.", + "version": "1", + "inputs": { + "personalAiConnectionKey": { + "type": "string", + "required": true, + "description": "Personal AI connection key", + "connection": { "platform": "personal-ai" } + }, + "text": { + "type": "string", + "required": true, + "description": "Message to send to the AI" + }, + "domainName": { + "type": "string", + "required": true, + "description": "The hyphenated domain name of the AI persona (found under the AI's name in Persona view)" + }, + "context": { + "type": "string", + "required": false, + "description": "Additional context for the AI response (e.g., 'Reply in one sentence')" + }, + "sessionId": { + "type": "string", + "required": false, + "description": "Session ID to continue an existing conversation" + }, + "userName": { + "type": "string", + "required": false, + "description": "Name of the user sending the message" + }, + "isStack": { + "type": "boolean", + "required": false, + "default": false, + "description": "Set true to also add the message to the AI's memory" + } + }, + "steps": [ + { + "id": "buildBody", + "name": "Build request body", + "type": "code", + "code": { + "source": "const { text, domainName, context, sessionId, userName, isStack } = $.input;\nif (!text || !domainName) throw new Error('text and domainName are required');\n\nconst body = { Text: text, DomainName: domainName };\nif (context) body.Context = context;\nif (sessionId) body.SessionId = sessionId;\nif (userName) body.UserName = userName;\nif (isStack) body.is_stack = true;\n\nreturn { body };" + } + }, + { + "id": "sendMessage", + "name": "Send message to Personal AI", + "type": "action", + "action": { + "platform": "personal-ai", + "actionId": "conn_mod_def::GJ6CmP04u9A::YxtvHt2ZSViHGpz4ddNrFQ", + "connectionKey": "$.input.personalAiConnectionKey", + "data": "$.steps.buildBody.output.body" + } + }, + { + "id": "formatResult", + "name": "Format AI response", + "type": "code", + "code": { + "source": "const resp = $.steps.sendMessage.response || {};\nreturn {\n aiMessage: resp.ai_message || '',\n aiScore: resp.ai_score || 0,\n aiName: resp.ai_name || '',\n sessionId: resp.SessionId || '',\n summary: resp.ai_message ? `${resp.ai_name || 'AI'} responded (score: ${(resp.ai_score || 0).toFixed(1)})` : 'No response received'\n};" + } + } + ] +} diff --git a/flows/postmark/README.md b/flows/postmark/README.md new file mode 100644 index 0000000..14332f5 --- /dev/null +++ b/flows/postmark/README.md @@ -0,0 +1,64 @@ +--- +name: postmark +description: | + Postmark transactional email flow for the One CLI. Send emails with HTML/text + bodies, tracking, CC/BCC, and custom headers. +triggers: + - "send email postmark" + - "postmark email" + - "transactional email" + - "/postmark" +--- + +# Postmark Flows + +Ready-to-run transactional email sending via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add postmark # Connect your Postmark account +one --agent list # Find your connection key +``` + +## Flows + +### Send Email + +Sends a single transactional email through Postmark. Supports HTML and plain text, +CC/BCC, open/click tracking, tags, and reply-to overrides. + +```bash +one flow execute postmark-send-email.flow.json \ + --input postmarkConnectionKey="" \ + --input from="sender@example.com" \ + --input to="recipient@example.com" \ + --input subject="Hello from Postmark" \ + --input textBody="Plain text message body" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `postmarkConnectionKey` | Yes | Your Postmark connection key | +| `from` | Yes | Sender email (must have confirmed Sender Signature) | +| `to` | Yes | Recipients, comma-separated (max 50) | +| `subject` | Yes | Email subject | +| `textBody` | No* | Plain text body | +| `htmlBody` | No* | HTML body | +| `cc` | No | CC recipients, comma-separated (max 50) | +| `bcc` | No | BCC recipients, comma-separated (max 50) | +| `replyTo` | No | Reply-To address override | +| `tag` | No | Tag for Postmark analytics | +| `trackOpens` | No | Enable open tracking | +| `trackLinks` | No | Click tracking: None, HtmlAndText, HtmlOnly, TextOnly | + +*At least one of `textBody` or `htmlBody` is required. + +**What it does under the hood:** + +1. Builds the Postmark email payload with proper field casing (From, To, Subject, etc.) +2. Sends via Postmark API (`POST /email`) +3. Returns message ID, submission timestamp, and delivery status diff --git a/flows/postmark/postmark-send-email.flow.json b/flows/postmark/postmark-send-email.flow.json new file mode 100644 index 0000000..389c63a --- /dev/null +++ b/flows/postmark/postmark-send-email.flow.json @@ -0,0 +1,99 @@ +{ + "key": "postmark-send-email", + "name": "Send Email via Postmark", + "description": "Send a transactional email through Postmark. Supports HTML and plain text bodies, CC/BCC, open/click tracking, custom headers, and attachments.", + "version": "1", + "inputs": { + "postmarkConnectionKey": { + "type": "string", + "required": true, + "description": "Postmark connection key", + "connection": { "platform": "postmark" } + }, + "from": { + "type": "string", + "required": true, + "description": "Sender email (must have a confirmed Sender Signature in Postmark)" + }, + "to": { + "type": "string", + "required": true, + "description": "Recipient email(s), comma-separated (max 50)" + }, + "subject": { + "type": "string", + "required": true, + "description": "Email subject line" + }, + "textBody": { + "type": "string", + "required": false, + "description": "Plain text email body" + }, + "htmlBody": { + "type": "string", + "required": false, + "description": "HTML email body" + }, + "cc": { + "type": "string", + "required": false, + "description": "CC recipients, comma-separated (max 50)" + }, + "bcc": { + "type": "string", + "required": false, + "description": "BCC recipients, comma-separated (max 50)" + }, + "replyTo": { + "type": "string", + "required": false, + "description": "Reply-To address override" + }, + "tag": { + "type": "string", + "required": false, + "description": "Tag for categorizing outgoing emails in Postmark analytics" + }, + "trackOpens": { + "type": "boolean", + "required": false, + "default": false, + "description": "Enable open tracking" + }, + "trackLinks": { + "type": "string", + "required": false, + "description": "Click tracking: None, HtmlAndText, HtmlOnly, TextOnly" + } + }, + "steps": [ + { + "id": "buildBody", + "name": "Build Postmark email payload", + "type": "code", + "code": { + "source": "const { from, to, subject, textBody, htmlBody, cc, bcc, replyTo, tag, trackOpens, trackLinks } = $.input;\nif (!from || !to || !subject) throw new Error('from, to, and subject are required');\nif (!textBody && !htmlBody) throw new Error('At least one of textBody or htmlBody is required');\n\nconst body = { From: from, To: to, Subject: subject };\nif (textBody) body.TextBody = textBody;\nif (htmlBody) body.HtmlBody = htmlBody;\nif (cc) body.Cc = cc;\nif (bcc) body.Bcc = bcc;\nif (replyTo) body.ReplyTo = replyTo;\nif (tag) body.Tag = tag;\nif (trackOpens) body.TrackOpens = true;\nif (trackLinks) body.TrackLinks = trackLinks;\n\nreturn { body };" + } + }, + { + "id": "sendEmail", + "name": "Send email via Postmark API", + "type": "action", + "action": { + "platform": "postmark", + "actionId": "conn_mod_def::GJ6JxSsWNaM::sWuMI7zXRNisv_3EaL6L-g", + "connectionKey": "$.input.postmarkConnectionKey", + "data": "$.steps.buildBody.output.body" + } + }, + { + "id": "formatResult", + "name": "Format send result", + "type": "code", + "code": { + "source": "const resp = $.steps.sendEmail.response || {};\nreturn {\n messageId: resp.MessageID || '',\n to: resp.To || $.input.to,\n submittedAt: resp.SubmittedAt || '',\n errorCode: resp.ErrorCode || 0,\n message: resp.Message || '',\n sent: resp.ErrorCode === 0 && !!resp.MessageID,\n summary: resp.MessageID ? `Sent \"${$.input.subject}\" to ${$.input.to} via Postmark` : `Failed: ${resp.Message || 'Unknown error'}`\n};" + } + } + ] +} diff --git a/flows/scrape-do/README.md b/flows/scrape-do/README.md new file mode 100644 index 0000000..88ede5b --- /dev/null +++ b/flows/scrape-do/README.md @@ -0,0 +1,56 @@ +--- +name: scrape-do +description: | + Scrape.do async web scraping flow for the One CLI. Create scraping jobs with + geo-targeting, device emulation, and markdown output for LLM workflows. +triggers: + - "scrape website" + - "web scraping" + - "scrape-do" + - "/scrape-do" +--- + +# Scrape.do Flows + +Ready-to-run async web scraping via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add scrape-do # Connect your Scrape.do account +one --agent list # Find your connection key +``` + +## Flows + +### Create Async Scraping Job + +Creates an asynchronous scraping job for one or more URLs. Returns a job ID and +task IDs for tracking. Supports geo-targeting, device emulation, residential +proxies, and markdown output (ideal for LLM pipelines). + +```bash +one flow execute scrape-do-async-job.flow.json \ + --input scrapeDoConnectionKey="" \ + --input targets='["https://example.com", "https://news.ycombinator.com"]' \ + --input output="markdown" \ + --input geoCode="US" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `scrapeDoConnectionKey` | Yes | Your Scrape.do connection key | +| `targets` | Yes | Array of URLs to scrape | +| `geoCode` | No | Country code for geo-targeting (e.g., US, GB, DE) | +| `device` | No | Device emulation: desktop, mobile, tablet | +| `output` | No | Output format: raw (default) or markdown | +| `super` | No | Set `true` for residential proxies | + +**What it does under the hood:** + +1. Builds scraping job configuration with targets and optional settings +2. Creates async job via Scrape.do API (`POST /api/v1/jobs`) +3. Returns job ID and task IDs for tracking progress via the job details endpoint diff --git a/flows/scrape-do/scrape-do-async-job.flow.json b/flows/scrape-do/scrape-do-async-job.flow.json new file mode 100644 index 0000000..046bdbe --- /dev/null +++ b/flows/scrape-do/scrape-do-async-job.flow.json @@ -0,0 +1,70 @@ +{ + "key": "scrape-do-async-job", + "name": "Create Async Scraping Job with Scrape.do", + "description": "Create an asynchronous web scraping job with Scrape.do. Supports multiple target URLs, geo-targeting, device emulation, rendering, proxy options, and markdown output for LLM workflows.", + "version": "1", + "inputs": { + "scrapeDoConnectionKey": { + "type": "string", + "required": true, + "description": "Scrape.do connection key", + "connection": { "platform": "scrape-do" } + }, + "targets": { + "type": "array", + "required": true, + "description": "Array of URLs to scrape" + }, + "geoCode": { + "type": "string", + "required": false, + "description": "Country code for geo-targeting (e.g., US, GB, DE)" + }, + "device": { + "type": "string", + "required": false, + "description": "Device to emulate: desktop, mobile, tablet" + }, + "output": { + "type": "string", + "required": false, + "default": "raw", + "description": "Output format: raw or markdown (markdown is ideal for LLM workflows)" + }, + "super": { + "type": "boolean", + "required": false, + "default": false, + "description": "Use residential proxies for harder-to-scrape sites" + } + }, + "steps": [ + { + "id": "buildBody", + "name": "Build scraping job request", + "type": "code", + "code": { + "source": "const { targets, geoCode, device, output, super: useSuper } = $.input;\nif (!targets || !targets.length) throw new Error('targets array is required with at least one URL');\n\nconst body = { Targets: targets };\nif (geoCode) body.GeoCode = geoCode;\nif (device) body.Device = device;\nif (output && output !== 'raw') body.Output = output;\nif (useSuper) body.Super = true;\n\nreturn { body };" + } + }, + { + "id": "createJob", + "name": "Create async scraping job", + "type": "action", + "action": { + "platform": "scrape-do", + "actionId": "conn_mod_def::GJ6X_uiDhuc::kvRKDHEfSHKTRowl57IIPQ", + "connectionKey": "$.input.scrapeDoConnectionKey", + "data": "$.steps.buildBody.output.body" + } + }, + { + "id": "formatResult", + "name": "Format job creation result", + "type": "code", + "code": { + "source": "const resp = $.steps.createJob.response || {};\nreturn {\n jobId: resp.JobID || '',\n taskIds: resp.TaskIDs || [],\n message: resp.Message || '',\n targetCount: $.input.targets.length,\n summary: resp.JobID ? `Created scraping job ${resp.JobID} with ${(resp.TaskIDs || []).length} task(s) for ${$.input.targets.length} URL(s)` : 'Failed to create scraping job'\n};" + } + } + ] +} diff --git a/flows/serp-api/README.md b/flows/serp-api/README.md new file mode 100644 index 0000000..7e2eb73 --- /dev/null +++ b/flows/serp-api/README.md @@ -0,0 +1,69 @@ +--- +name: serp-api +description: | + SerpApi Google search flow for the One CLI. Run structured Google searches with + organic results, local pack, knowledge graph, and location targeting. +triggers: + - "google search" + - "serp search" + - "serpapi" + - "search google" + - "/serp-api" +--- + +# SerpApi Flows + +Ready-to-run Google search via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add serp-api # Connect your SerpApi account +one --agent list # Find your connection key +``` + +## Flows + +### Google Search + +Runs a Google search and returns structured SERP results including organic results, +local pack, knowledge graph, and related searches. Supports location targeting, +language, pagination, and Google search operators. + +```bash +one flow execute serp-api-google-search.flow.json \ + --input serpApiConnectionKey="" \ + --input query="best coffee shops" \ + --input location="Austin, Texas, United States" \ + --input gl="us" \ + --input hl="en" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `serpApiConnectionKey` | Yes | Your SerpApi connection key | +| `query` | Yes | Search query (supports site:, inurl:, intitle: operators) | +| `location` | No | Location to search from (e.g., "Austin, Texas, United States") | +| `gl` | No | Country code (e.g., us, uk, fr) | +| `hl` | No | Language code (e.g., en, es, fr) | +| `num` | No | Results per page (default: 10) | +| `start` | No | Offset for pagination (0 = first page, 10 = second) | + +**What it does under the hood:** + +1. Builds search parameters with query, location, and language settings +2. Calls SerpApi Google engine (`GET /search.json?engine=google`) +3. Extracts organic results, local results, knowledge graph, and related searches +4. Returns clean, structured data ready for analysis or downstream use + +**Search query examples:** + +| Query | Meaning | +|-------|---------| +| `site:example.com ai tools` | Search within a specific site | +| `intitle:review "macbook pro"` | Title must contain "review" | +| `filetype:pdf machine learning` | Find PDF documents | +| `"exact phrase" -exclude` | Exact match, excluding a term | diff --git a/flows/serp-api/serp-api-google-search.flow.json b/flows/serp-api/serp-api-google-search.flow.json new file mode 100644 index 0000000..5aee33a --- /dev/null +++ b/flows/serp-api/serp-api-google-search.flow.json @@ -0,0 +1,75 @@ +{ + "key": "serp-api-google-search", + "name": "Google Search via SerpApi", + "description": "Run a Google search and get structured SERP results including organic results, local pack, knowledge graph, and related questions. Supports location targeting, language, pagination, and device emulation.", + "version": "1", + "inputs": { + "serpApiConnectionKey": { + "type": "string", + "required": true, + "description": "SerpApi connection key", + "connection": { "platform": "serp-api" } + }, + "query": { + "type": "string", + "required": true, + "description": "Search query (supports Google operators like site:, inurl:, intitle:)" + }, + "location": { + "type": "string", + "required": false, + "description": "Location to search from (e.g., 'Austin, Texas, United States')" + }, + "gl": { + "type": "string", + "required": false, + "description": "Country code (e.g., us, uk, fr)" + }, + "hl": { + "type": "string", + "required": false, + "description": "Language code (e.g., en, es, fr)" + }, + "num": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of results per page" + }, + "start": { + "type": "number", + "required": false, + "default": 0, + "description": "Result offset for pagination (0 = first page, 10 = second page)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build search query parameters", + "type": "code", + "code": { + "source": "const { query, location, gl, hl, num, start } = $.input;\nif (!query) throw new Error('query is required');\n\nconst params = { engine: 'google', q: query };\nif (location) params.location = location;\nif (gl) params.gl = gl;\nif (hl) params.hl = hl;\nif (num && num !== 10) params.num = String(num);\nif (start) params.start = String(start);\n\nreturn { params };" + } + }, + { + "id": "search", + "name": "Execute Google search via SerpApi", + "type": "action", + "action": { + "platform": "serp-api", + "actionId": "conn_mod_def::GJ6Zy-104j8::od3CVwSAS0GWFerwMQjcKw", + "connectionKey": "$.input.serpApiConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Extract and format search results", + "type": "code", + "code": { + "source": "const resp = $.steps.search.response || {};\nconst info = resp.search_information || {};\nconst organic = (resp.organic_results || []).map(r => ({\n position: r.position,\n title: r.title || '',\n link: r.link || '',\n snippet: r.snippet || '',\n displayedLink: r.displayed_link || ''\n}));\n\nconst localResults = (resp.local_results?.places || []).map(p => ({\n title: p.title || '',\n type: p.type || '',\n address: p.address || '',\n rating: p.rating || null,\n reviews: p.reviews || 0\n}));\n\nconst knowledgeGraph = resp.knowledge_graph ? {\n title: resp.knowledge_graph.title || '',\n type: resp.knowledge_graph.type || '',\n description: resp.knowledge_graph.description || ''\n} : null;\n\nreturn {\n query: $.input.query,\n totalResults: info.total_results || 0,\n organicResults: organic,\n localResults,\n knowledgeGraph,\n relatedSearches: (resp.related_searches || []).map(r => r.query || ''),\n summary: `Found ${organic.length} organic result${organic.length === 1 ? '' : 's'} for \"${$.input.query}\"${info.total_results ? ` (${info.total_results.toLocaleString()} total)` : ''}`\n};" + } + } + ] +} diff --git a/flows/shippo/README.md b/flows/shippo/README.md new file mode 100644 index 0000000..4666075 --- /dev/null +++ b/flows/shippo/README.md @@ -0,0 +1,111 @@ +--- +name: shippo +description: | + Shippo shipping flows for the One CLI. Create shipments to get carrier rates, + purchase shipping labels, and track packages across carriers. +triggers: + - "create shipment" + - "shipping label" + - "track package" + - "shippo" + - "/shippo" +--- + +# Shippo Flows + +Ready-to-run shipping workflows via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add shippo # Connect your Shippo account +one --agent list # Find your connection key +``` + +## Flows + +### Create Shipment + +Creates a shipment with sender/recipient addresses and package dimensions. +Returns available shipping rates from multiple carriers (USPS, FedEx, UPS, etc.). + +```bash +one flow execute shippo-create-shipment.flow.json \ + --input shippoConnectionKey="" \ + --input fromName="Jane Doe" \ + --input fromStreet1="123 Main St" \ + --input fromCity="San Francisco" \ + --input fromState="CA" \ + --input fromZip="94105" \ + --input fromCountry="US" \ + --input toName="John Smith" \ + --input toStreet1="456 Oak Ave" \ + --input toCity="New York" \ + --input toState="NY" \ + --input toZip="10001" \ + --input toCountry="US" \ + --input length="10" \ + --input width="7" \ + --input height="4" \ + --input weight="1" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `shippoConnectionKey` | Yes | Your Shippo connection key | +| `fromName/Street1/City/State/Zip/Country` | Yes | Sender address fields | +| `toName/Street1/City/State/Zip/Country` | Yes | Recipient address fields | +| `length`, `width`, `height` | Yes | Package dimensions | +| `weight` | Yes | Package weight | +| `distanceUnit` | No | Unit: in (default) or cm | +| `massUnit` | No | Unit: lb (default), kg, g, oz | + +### Create Shipping Label + +Purchases a shipping label for a specific rate. Use this after `create-shipment` +to buy the label. + +```bash +one flow execute shippo-create-label.flow.json \ + --input shippoConnectionKey="" \ + --input rateId="" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `shippoConnectionKey` | Yes | Your Shippo connection key | +| `rateId` | Yes | Rate object ID from a shipment | +| `labelFileType` | No | Format: PDF (default), PNG, PDF_4x6, ZPLII | +| `async` | No | Create label asynchronously | + +### Track Package + +Gets tracking status and history for any package. Auto-detects Shippo test +tracking numbers (SHIPPO_TRANSIT, SHIPPO_DELIVERED, etc.). + +```bash +one flow execute shippo-track-package.flow.json \ + --input shippoConnectionKey="" \ + --input carrier="usps" \ + --input trackingNumber="9205590164917312751089" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `shippoConnectionKey` | Yes | Your Shippo connection key | +| `carrier` | Yes | Carrier: usps, fedex, ups, dhl_express, shippo (test) | +| `trackingNumber` | Yes | Tracking number | + +## Typical Workflow + +1. **Create shipment** to get rates from multiple carriers +2. **Pick a rate** based on price, speed, or carrier preference +3. **Create label** with the chosen rate ID +4. **Track package** using the returned tracking number diff --git a/flows/shippo/shippo-create-label.flow.json b/flows/shippo/shippo-create-label.flow.json new file mode 100644 index 0000000..cca18e4 --- /dev/null +++ b/flows/shippo/shippo-create-label.flow.json @@ -0,0 +1,60 @@ +{ + "key": "shippo-create-label", + "name": "Create Shipping Label via Shippo", + "description": "Purchase a shipping label for a rate obtained from a shipment. Returns label URL, tracking number, and tracking status.", + "version": "1", + "inputs": { + "shippoConnectionKey": { + "type": "string", + "required": true, + "description": "Shippo connection key", + "connection": { "platform": "shippo" } + }, + "rateId": { + "type": "string", + "required": true, + "description": "Rate object ID from a shipment (returned by shippo-create-shipment flow)" + }, + "labelFileType": { + "type": "string", + "required": false, + "default": "PDF", + "description": "Label format: PNG, PDF, PDF_4x6, PDF_4x8, PDF_A4, ZPLII" + }, + "async": { + "type": "boolean", + "required": false, + "default": false, + "description": "Create label asynchronously" + } + }, + "steps": [ + { + "id": "buildBody", + "name": "Build label purchase body", + "type": "code", + "code": { + "source": "const body = { rate: $.input.rateId };\nif ($.input.labelFileType && $.input.labelFileType !== 'PDF') body.label_file_type = $.input.labelFileType;\nif ($.input.async) body.async = true;\nreturn { body };" + } + }, + { + "id": "createLabel", + "name": "Purchase shipping label", + "type": "action", + "action": { + "platform": "shippo", + "actionId": "conn_mod_def::GEf2JxxcO-0::CtOe988KTDu8o91yzx8n0Q", + "connectionKey": "$.input.shippoConnectionKey", + "data": "$.steps.buildBody.output.body" + } + }, + { + "id": "formatResult", + "name": "Format label result", + "type": "code", + "code": { + "source": "const resp = $.steps.createLabel.response || {};\nreturn {\n labelId: resp.object_id || '',\n state: resp.object_state || '',\n trackingNumber: resp.tracking_number || '',\n labelUrl: resp.label_url || '',\n commercialInvoiceUrl: resp.commercial_invoice_url || '',\n eta: resp.eta || '',\n summary: resp.object_id ? `Label ${resp.object_id} created (tracking: ${resp.tracking_number || 'pending'})` : 'Failed to create label'\n};" + } + } + ] +} diff --git a/flows/shippo/shippo-create-shipment.flow.json b/flows/shippo/shippo-create-shipment.flow.json new file mode 100644 index 0000000..221accc --- /dev/null +++ b/flows/shippo/shippo-create-shipment.flow.json @@ -0,0 +1,137 @@ +{ + "key": "shippo-create-shipment", + "name": "Create Shipment and Get Rates via Shippo", + "description": "Create a new shipment with sender/recipient addresses and package dimensions via Shippo. Returns available shipping rates from multiple carriers. The parcels field is always sent as an array to prevent common 400 errors.", + "version": "1", + "inputs": { + "shippoConnectionKey": { + "type": "string", + "required": true, + "description": "Shippo connection key", + "connection": { "platform": "shippo" } + }, + "fromName": { + "type": "string", + "required": true, + "description": "Sender name" + }, + "fromStreet1": { + "type": "string", + "required": true, + "description": "Sender street address" + }, + "fromCity": { + "type": "string", + "required": true, + "description": "Sender city" + }, + "fromState": { + "type": "string", + "required": true, + "description": "Sender state" + }, + "fromZip": { + "type": "string", + "required": true, + "description": "Sender ZIP code" + }, + "fromCountry": { + "type": "string", + "required": true, + "default": "US", + "description": "Sender country code (e.g., US)" + }, + "toName": { + "type": "string", + "required": true, + "description": "Recipient name" + }, + "toStreet1": { + "type": "string", + "required": true, + "description": "Recipient street address" + }, + "toCity": { + "type": "string", + "required": true, + "description": "Recipient city" + }, + "toState": { + "type": "string", + "required": true, + "description": "Recipient state" + }, + "toZip": { + "type": "string", + "required": true, + "description": "Recipient ZIP code" + }, + "toCountry": { + "type": "string", + "required": true, + "default": "US", + "description": "Recipient country code" + }, + "length": { + "type": "string", + "required": true, + "description": "Package length" + }, + "width": { + "type": "string", + "required": true, + "description": "Package width" + }, + "height": { + "type": "string", + "required": true, + "description": "Package height" + }, + "distanceUnit": { + "type": "string", + "required": false, + "default": "in", + "description": "Distance unit: in or cm" + }, + "weight": { + "type": "string", + "required": true, + "description": "Package weight" + }, + "massUnit": { + "type": "string", + "required": false, + "default": "lb", + "description": "Mass unit: lb, kg, g, oz" + } + }, + "steps": [ + { + "id": "buildShipment", + "name": "Build shipment request body", + "type": "code", + "code": { + "source": "const i = $.input;\nreturn {\n body: {\n address_from: {\n name: i.fromName,\n street1: i.fromStreet1,\n city: i.fromCity,\n state: i.fromState,\n zip: i.fromZip,\n country: i.fromCountry || 'US'\n },\n address_to: {\n name: i.toName,\n street1: i.toStreet1,\n city: i.toCity,\n state: i.toState,\n zip: i.toZip,\n country: i.toCountry || 'US'\n },\n parcels: [{\n length: i.length,\n width: i.width,\n height: i.height,\n distance_unit: i.distanceUnit || 'in',\n weight: i.weight,\n mass_unit: i.massUnit || 'lb'\n }]\n }\n};" + } + }, + { + "id": "createShipment", + "name": "Create shipment via Shippo", + "type": "action", + "action": { + "platform": "shippo", + "actionId": "conn_mod_def::GEf2IVsPhus::k5I9cS3nSdGlXpRyTGBQDw", + "connectionKey": "$.input.shippoConnectionKey", + "data": "$.steps.buildShipment.output.body" + } + }, + { + "id": "formatResult", + "name": "Format shipment and rates", + "type": "code", + "code": { + "source": "const resp = $.steps.createShipment.response || {};\nconst rates = (resp.rates || []).map(r => ({\n provider: r.provider || '',\n servicelevel: r.servicelevel?.name || r.servicelevel_name || '',\n amount: r.amount || '',\n currency: r.currency || 'USD',\n estimatedDays: r.estimated_days || r.days || null,\n objectId: r.object_id || ''\n}));\n\nreturn {\n shipmentId: resp.object_id || '',\n status: resp.status || '',\n rates,\n rateCount: rates.length,\n from: `${$.input.fromCity}, ${$.input.fromState}`,\n to: `${$.input.toCity}, ${$.input.toState}`,\n summary: resp.object_id ? `Shipment ${resp.object_id} created with ${rates.length} rate${rates.length === 1 ? '' : 's'} available` : 'Failed to create shipment'\n};" + } + } + ] +} diff --git a/flows/shippo/shippo-track-package.flow.json b/flows/shippo/shippo-track-package.flow.json new file mode 100644 index 0000000..743f102 --- /dev/null +++ b/flows/shippo/shippo-track-package.flow.json @@ -0,0 +1,56 @@ +{ + "key": "shippo-track-package", + "name": "Track Package via Shippo", + "description": "Get tracking status and history for a package. Automatically detects Shippo test tracking numbers (SHIPPO_*) and routes them correctly.", + "version": "1", + "inputs": { + "shippoConnectionKey": { + "type": "string", + "required": true, + "description": "Shippo connection key", + "connection": { "platform": "shippo" } + }, + "carrier": { + "type": "string", + "required": true, + "description": "Carrier name in lowercase (e.g., usps, fedex, ups, dhl_express, shippo for test)" + }, + "trackingNumber": { + "type": "string", + "required": true, + "description": "Tracking number for the shipment" + } + }, + "steps": [ + { + "id": "resolveCarrier", + "name": "Auto-detect test tracking numbers", + "type": "code", + "code": { + "source": "let carrier = $.input.carrier.toLowerCase();\nconst trackingNumber = $.input.trackingNumber;\n\n// Auto-detect Shippo test tracking numbers\nif (trackingNumber.startsWith('SHIPPO_')) carrier = 'shippo';\n\nreturn { carrier, trackingNumber };" + } + }, + { + "id": "getTracking", + "name": "Fetch tracking status", + "type": "action", + "action": { + "platform": "shippo", + "actionId": "conn_mod_def::GEf3AzbqJoI::Qg5-X5sFReKlrZ8OAcXnig", + "connectionKey": "$.input.shippoConnectionKey", + "pathVars": { + "Carrier": "$.steps.resolveCarrier.output.carrier", + "TrackingNumber": "$.steps.resolveCarrier.output.trackingNumber" + } + } + }, + { + "id": "formatResult", + "name": "Format tracking result", + "type": "code", + "code": { + "source": "const resp = $.steps.getTracking.response || {};\nconst status = resp.tracking_status || {};\nconst history = (resp.tracking_history || []).map(h => ({\n status: h.status || '',\n details: h.status_details || '',\n date: h.status_date || '',\n location: h.location ? `${h.location.city || ''}, ${h.location.state || ''} ${h.location.zip || ''}`.trim() : ''\n}));\n\nreturn {\n trackingNumber: resp.tracking_number || $.input.trackingNumber,\n carrier: resp.carrier || $.input.carrier,\n status: status.status || 'UNKNOWN',\n statusDetails: status.status_details || '',\n statusDate: status.status_date || '',\n location: status.location ? `${status.location.city || ''}, ${status.location.state || ''}` : '',\n eta: resp.eta || '',\n servicelevel: resp.servicelevel?.name || '',\n history,\n summary: `${resp.tracking_number || $.input.trackingNumber}: ${status.status || 'UNKNOWN'} - ${status.status_details || 'No details available'}`\n};" + } + } + ] +} diff --git a/flows/stripe/README.md b/flows/stripe/README.md new file mode 100644 index 0000000..12db525 --- /dev/null +++ b/flows/stripe/README.md @@ -0,0 +1,226 @@ +--- +name: stripe +description: | + Stripe integration flows for the One CLI. Ready-to-run workflows that handle + response compression (stripping 30+ verbose fields per object), form encoding, + query parameter construction, date range filtering, and pagination. +triggers: + - "stripe" + - "/stripe" + - "list invoices" + - "get invoice" + - "payment intents" + - "checkout sessions" + - "connected accounts" + - "create webhook" + - "stripe webhook" +--- + +# Stripe Flows + +Ready-to-run workflows for Stripe via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add stripe # Connect your Stripe account +one --agent list # Find your connection key +``` + +## Flows + +### Create Webhook Endpoint + +Creates a Stripe webhook endpoint. Handles form-encoding of enabled events +arrays and metadata key-value pairs that Stripe's API requires. + +```bash +one flow execute stripe-create-webhook.flow.json \ + --input stripeConnectionKey="" \ + --input url="https://your-app.com/webhooks/stripe" \ + --input enabledEvents='["charge.succeeded", "invoice.paid", "customer.subscription.updated"]' \ + --input description="Production webhook" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key | +| `url` | Yes | URL to receive webhook POSTs | +| `enabledEvents` | Yes | Event types to subscribe to. Use `["*"]` for all events | +| `description` | No | Human-readable description | +| `connect` | No | Receive events from connected accounts (Stripe Connect) | +| `metadata` | No | Key-value metadata object | + +**What it does under the hood:** + +1. Builds form-encoded body with array-indexed event types (`enabled_events[0]`, `enabled_events[1]`, etc.) +2. Encodes metadata as `metadata[key]=value` pairs +3. POSTs to `/v1/webhook_endpoints` +4. Returns the webhook ID, signing secret, and status + +--- + +### List Checkout Sessions + +Lists Stripe Checkout sessions with filtering. Compresses responses by stripping +40+ verbose fields (consent, shipping, branding, UI config, etc.) while +preserving payment status, amounts, customer, and line items. + +```bash +one flow execute stripe-get-checkout-sessions.flow.json \ + --input stripeConnectionKey="" \ + --input status="complete" \ + --input limit=20 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key | +| `customer` | No | Filter by customer ID | +| `status` | No | `complete`, `expired`, or `open` | +| `paymentIntent` | No | Filter by payment intent ID | +| `subscription` | No | Filter by subscription ID | +| `customerEmail` | No | Filter by customer email | +| `limit` | No | Number to return (1-100, default 10) | +| `startingAfter` | No | Pagination cursor | +| `expand` | No | Fields to expand (e.g., `["data.line_items"]`) | + +--- + +### List Invoices + +Lists Stripe invoices with filtering by status, customer, date range, and +collection method. Compresses responses by removing 30+ fields per invoice +(metadata, payment settings, tax details, shipping, etc.). + +```bash +one flow execute stripe-get-invoices.flow.json \ + --input stripeConnectionKey="" \ + --input status="open" \ + --input customer="cus_abc123" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key | +| `status` | No | `draft`, `open`, `paid`, `uncollectible`, or `void` | +| `customer` | No | Filter by customer ID | +| `subscription` | No | Filter by subscription ID | +| `collectionMethod` | No | `charge_automatically` or `send_invoice` | +| `createdAfter` | No | Unix timestamp -- only invoices created after | +| `createdBefore` | No | Unix timestamp -- only invoices created before | +| `limit` | No | Number to return (1-100, default 10) | +| `startingAfter` | No | Pagination cursor | + +--- + +### Get Invoice Detail + +Retrieves a single invoice with full line item data. Same compression as the +list flow, applied to one invoice. + +```bash +one flow execute stripe-get-invoice-detail.flow.json \ + --input stripeConnectionKey="" \ + --input invoiceId="in_1abc..." +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key | +| `invoiceId` | Yes | The invoice ID | +| `expand` | No | Fields to expand (e.g., `["customer", "subscription"]`) | + +--- + +### List Payment Intents + +Lists payment intents with customer and date filtering. Strips internal fields +(client_secret, payment_method_options, shipping, transfer data, etc.). + +```bash +one flow execute stripe-get-payment-intents.flow.json \ + --input stripeConnectionKey="" \ + --input customer="cus_abc123" \ + --input limit=25 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key | +| `customer` | No | Filter by customer ID | +| `createdAfter` | No | Unix timestamp | +| `createdBefore` | No | Unix timestamp | +| `limit` | No | Number to return (1-100, default 10) | +| `startingAfter` | No | Pagination cursor | + +--- + +### Get Payment Intent Detail + +Retrieves a single payment intent by ID. Supports expand and client secret +params for client-side usage. + +```bash +one flow execute stripe-get-payment-intent.flow.json \ + --input stripeConnectionKey="" \ + --input intentId="pi_1abc..." +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key | +| `intentId` | Yes | The payment intent ID | +| `clientSecret` | No | Client secret for client-side retrieval | +| `expand` | No | Fields to expand (e.g., `["customer", "payment_method"]`) | + +--- + +### List Connected Accounts + +Lists connected accounts (Stripe Connect). Compresses deeply nested settings, +requirements, and dashboard fields while preserving account identity, +capabilities, and payout status. + +```bash +one flow execute stripe-get-connected-accounts.flow.json \ + --input stripeConnectionKey="" \ + --input limit=50 +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `stripeConnectionKey` | Yes | Your Stripe connection key (platform account) | +| `createdAfter` | No | Unix timestamp | +| `createdBefore` | No | Unix timestamp | +| `limit` | No | Number to return (1-100, default 10) | +| `startingAfter` | No | Pagination cursor | +| `expand` | No | Fields to expand | + +## Adapting These Flows + +These flows are templates. Fork and modify them for your use case: + +- **Revenue dashboard**: Chain `stripe-get-payment-intents` with date filters into a Slack or Notion summary. +- **Overdue invoice alerts**: Use `stripe-get-invoices` with `status=open` and pipe results to email or Slack. +- **Checkout analytics**: Use `stripe-get-checkout-sessions` with `expand=["data.line_items"]` and aggregate in a code step. +- **Connect onboarding monitor**: Use `stripe-get-connected-accounts` and filter by `requirements.currently_due` to find accounts needing action. +- **Webhook setup automation**: Use `stripe-create-webhook` as part of a deployment flow to register endpoints per environment. +- **Invoice + line items**: Chain `stripe-get-invoices` into a loop of `stripe-get-invoice-detail` calls for full line-item data. + +The orchestration knowledge is in the flow's `code` steps. Read them to understand the compression logic, query parameter construction, and date filter patterns -- then build your own variations. diff --git a/flows/stripe/stripe-create-webhook.flow.json b/flows/stripe/stripe-create-webhook.flow.json new file mode 100644 index 0000000..68c4a1d --- /dev/null +++ b/flows/stripe/stripe-create-webhook.flow.json @@ -0,0 +1,68 @@ +{ + "key": "stripe-create-webhook", + "name": "Create Stripe Webhook Endpoint", + "description": "Creates a Stripe webhook endpoint. Handles form-encoding of enabled events, metadata, and expand parameters that Stripe's API requires.", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key", + "connection": { "platform": "stripe" } + }, + "url": { + "type": "string", + "required": true, + "description": "The URL the webhook will POST events to" + }, + "enabledEvents": { + "type": "array", + "required": true, + "description": "List of event types to subscribe to (e.g., ['charge.succeeded', 'invoice.paid']). Use ['*'] for all events." + }, + "description": { + "type": "string", + "required": false, + "description": "Human-readable description for this webhook endpoint" + }, + "connect": { + "type": "boolean", + "required": false, + "description": "Whether to receive events from connected accounts (Stripe Connect)" + }, + "metadata": { + "type": "object", + "required": false, + "description": "Key-value metadata to attach to the webhook endpoint" + } + }, + "steps": [ + { + "id": "buildFormData", + "name": "Build form-encoded data for Stripe's webhook API", + "type": "code", + "code": { + "source": "const { url, enabledEvents, description, connect, metadata } = $.input;\n\nif (!url) throw new Error('url is required');\nif (!enabledEvents || !enabledEvents.length) throw new Error('enabledEvents is required');\n\nconst formData = { url };\n\nenabledEvents.forEach((event, i) => {\n formData[`enabled_events[${i}]`] = event;\n});\n\nif (description) formData.description = description;\nif (connect !== undefined) formData.connect = String(connect);\n\nif (metadata && typeof metadata === 'object') {\n Object.entries(metadata).forEach(([key, value]) => {\n formData[`metadata[${key}]`] = String(value);\n });\n}\n\nreturn { formData };" + } + }, + { + "id": "createWebhook", + "name": "Create the webhook endpoint via Stripe API", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEhAhiY8zw::Balmyr0TQvuRV-kJGUO1Zw", + "connectionKey": "$.input.stripeConnectionKey", + "data": "$.steps.buildFormData.output.formData" + } + }, + { + "id": "formatResult", + "name": "Format the response with webhook secret", + "type": "code", + "code": { + "source": "const resp = $.steps.createWebhook.response || {};\n\nif (resp.error) {\n return {\n success: false,\n error: resp.error.message || 'Failed to create webhook endpoint',\n details: resp.error\n };\n}\n\nreturn {\n success: true,\n webhookId: resp.id || '',\n secret: resp.secret || '',\n url: resp.url || '',\n status: resp.status || '',\n enabledEvents: resp.enabled_events || [],\n livemode: resp.livemode || false,\n summary: resp.id\n ? `Created webhook ${resp.id} for ${(resp.enabled_events || []).length} event type(s). SAVE THE SECRET -- it won't be shown again.`\n : 'Failed to create webhook endpoint'\n};" + } + } + ] +} diff --git a/flows/stripe/stripe-get-checkout-sessions.flow.json b/flows/stripe/stripe-get-checkout-sessions.flow.json new file mode 100644 index 0000000..bfac6d8 --- /dev/null +++ b/flows/stripe/stripe-get-checkout-sessions.flow.json @@ -0,0 +1,84 @@ +{ + "key": "stripe-get-checkout-sessions", + "name": "List Stripe Checkout Sessions", + "description": "Lists Stripe Checkout sessions with filtering and response compression. Strips verbose fields (consent, shipping, branding, etc.) to return only actionable data.", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key", + "connection": { "platform": "stripe" } + }, + "customer": { + "type": "string", + "required": false, + "description": "Filter by customer ID" + }, + "status": { + "type": "string", + "required": false, + "description": "Filter by status: 'complete', 'expired', or 'open'" + }, + "paymentIntent": { + "type": "string", + "required": false, + "description": "Filter by payment intent ID" + }, + "subscription": { + "type": "string", + "required": false, + "description": "Filter by subscription ID" + }, + "customerEmail": { + "type": "string", + "required": false, + "description": "Filter by customer email address" + }, + "limit": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of sessions to return (1-100)" + }, + "startingAfter": { + "type": "string", + "required": false, + "description": "Cursor for pagination (session ID to start after)" + }, + "expand": { + "type": "array", + "required": false, + "description": "Fields to expand (e.g., ['data.line_items', 'data.customer'])" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { customer, status, paymentIntent, subscription, customerEmail, limit, startingAfter, expand } = $.input;\n\nconst params = {};\nif (customer) params.customer = customer;\nif (status) params.status = status;\nif (paymentIntent) params.payment_intent = paymentIntent;\nif (subscription) params.subscription = subscription;\nif (customerEmail) params['customer_details[email]'] = customerEmail;\nif (limit) params.limit = String(Math.max(1, Math.min(100, limit)));\nif (startingAfter) params.starting_after = startingAfter;\n\nif (expand && expand.length > 0) {\n expand.forEach((field, i) => {\n params[`expand[${i}]`] = field;\n });\n}\n\nreturn { params };" + } + }, + { + "id": "listSessions", + "name": "Fetch checkout sessions from Stripe", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEfo0fqxOg::gT8BU_pcQ-edqwUALv8Khw", + "connectionKey": "$.input.stripeConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "compressResults", + "name": "Strip verbose fields and compress the response", + "type": "code", + "code": { + "source": "const resp = $.steps.listSessions.response || {};\nconst sessions = resp.data || [];\n\nconst sessionKeysToRemove = [\n 'object', 'adaptive_pricing', 'after_expiration', 'branding_settings',\n 'client_secret', 'collected_information', 'consent', 'consent_collection',\n 'currency_conversion', 'custom_fields', 'custom_text', 'customer_creation',\n 'customer_details', 'discounts', 'excluded_payment_method_types',\n 'invoice_creation', 'livemode', 'locale', 'name_collection',\n 'optional_items', 'origin_context', 'payment_method_configuration_details',\n 'payment_method_options', 'permissions', 'presentment_details',\n 'recovered_from', 'redirect_on_completion', 'return_url',\n 'saved_payment_method_options', 'shipping_address_collection',\n 'shipping_cost', 'shipping_details', 'shipping_options', 'submit_type',\n 'tax_id_collection', 'total_details', 'ui_mode', 'wallet_options'\n];\n\nconst lineItemKeysToRemove = ['object', 'amount_discount', 'amount_tax', 'discounts', 'price', 'taxes'];\n\nfunction compress(session) {\n const s = { ...session };\n sessionKeysToRemove.forEach(k => delete s[k]);\n\n if (s.line_items && s.line_items.data) {\n delete s.line_items.has_more;\n delete s.line_items.url;\n s.line_items.data = s.line_items.data.map(item => {\n const li = { ...item };\n lineItemKeysToRemove.forEach(k => delete li[k]);\n return li;\n });\n }\n\n if (s.automatic_tax) {\n delete s.automatic_tax.liability;\n delete s.automatic_tax.provider;\n }\n\n return s;\n}\n\nconst compressed = sessions.map(compress);\n\nreturn {\n sessions: compressed,\n totalReturned: compressed.length,\n hasMore: resp.has_more || false,\n summary: `Retrieved ${compressed.length} checkout session${compressed.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/stripe/stripe-get-connected-accounts.flow.json b/flows/stripe/stripe-get-connected-accounts.flow.json new file mode 100644 index 0000000..b5be6d2 --- /dev/null +++ b/flows/stripe/stripe-get-connected-accounts.flow.json @@ -0,0 +1,69 @@ +{ + "key": "stripe-get-connected-accounts", + "name": "List Stripe Connected Accounts", + "description": "Lists connected accounts (Stripe Connect). Compresses responses by removing deeply nested settings, requirements, and dashboard fields while preserving account identity, capabilities, and status.", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key (must be a platform/Connect account)", + "connection": { "platform": "stripe" } + }, + "createdAfter": { + "type": "number", + "required": false, + "description": "Filter accounts created after this Unix timestamp" + }, + "createdBefore": { + "type": "number", + "required": false, + "description": "Filter accounts created before this Unix timestamp" + }, + "limit": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of accounts to return (1-100)" + }, + "startingAfter": { + "type": "string", + "required": false, + "description": "Cursor for pagination (account ID to start after)" + }, + "expand": { + "type": "array", + "required": false, + "description": "Fields to expand" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { createdAfter, createdBefore, limit, startingAfter, expand } = $.input;\n\nconst params = {};\nif (createdAfter) params['created[gte]'] = String(createdAfter);\nif (createdBefore) params['created[lte]'] = String(createdBefore);\nif (limit) params.limit = String(Math.max(1, Math.min(100, limit)));\nif (startingAfter) params.starting_after = startingAfter;\n\nif (expand && expand.length > 0) {\n expand.forEach((field, i) => {\n params[`expand[${i}]`] = field;\n });\n}\n\nreturn { params };" + } + }, + { + "id": "listAccounts", + "name": "Fetch connected accounts from Stripe", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEfXXy9PlM::C9ElluwuTJ6t55dex_Y0tA", + "connectionKey": "$.input.stripeConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "compressResults", + "name": "Strip deeply nested verbose fields from each account", + "type": "code", + "code": { + "source": "const resp = $.steps.listAccounts.response || {};\nconst accounts = resp.data || [];\n\nfunction deleteNested(obj, path) {\n const parts = path.split('.');\n let current = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current || typeof current !== 'object') return;\n current = current[parts[i]];\n }\n if (current && typeof current === 'object') {\n delete current[parts[parts.length - 1]];\n }\n}\n\nconst keysToRemove = [\n 'business_profile.annual_revenue', 'business_profile.estimated_worker_count',\n 'business_profile.support_address', 'business_profile.support_email',\n 'business_profile.support_phone', 'business_profile.support_url',\n 'controller.fees', 'controller.losses', 'controller.requirement_collection',\n 'controller.stripe_dashboard',\n 'external_accounts.has_more', 'external_accounts.total_count', 'external_accounts.url',\n 'future_requirements.alternatives', 'future_requirements.current_deadline',\n 'future_requirements.disabled_reason', 'future_requirements.errors',\n 'future_requirements.past_due', 'future_requirements.pending_verification',\n 'login_links',\n 'requirements.alternatives', 'requirements.current_deadline',\n 'requirements.errors', 'requirements.pending_verification',\n 'settings.bacs_debit_payments', 'settings.branding', 'settings.card_issuing',\n 'settings.card_payments.decline_on', 'settings.card_payments.statement_descriptor_prefix',\n 'settings.card_payments.statement_descriptor_prefix_kanji',\n 'settings.card_payments.statement_descriptor_prefix_kana',\n 'settings.dashboard.display_name', 'settings.dashboard.timezone',\n 'settings.invoices',\n 'settings.payments.statement_descriptor', 'settings.payments.statement_descriptor_kana',\n 'settings.payments.statement_descriptor_kanji',\n 'settings.payouts.debit_negative_balances', 'settings.payouts.schedule',\n 'settings.payouts.statement_descriptor', 'settings.sepa_debit_payments',\n 'tos_acceptance.service_agreement', 'tos_acceptance.user_agent'\n];\n\nconst compressed = accounts.map(account => {\n const c = JSON.parse(JSON.stringify(account));\n keysToRemove.forEach(path => deleteNested(c, path));\n return c;\n});\n\nreturn {\n accounts: compressed,\n totalReturned: compressed.length,\n hasMore: resp.has_more || false,\n summary: `Retrieved ${compressed.length} connected account${compressed.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/stripe/stripe-get-invoice-detail.flow.json b/flows/stripe/stripe-get-invoice-detail.flow.json new file mode 100644 index 0000000..834e797 --- /dev/null +++ b/flows/stripe/stripe-get-invoice-detail.flow.json @@ -0,0 +1,56 @@ +{ + "key": "stripe-get-invoice-detail", + "name": "Get Stripe Invoice Detail", + "description": "Retrieves a single Stripe invoice by ID with compressed output. Strips verbose metadata while preserving line items, amounts, customer info, and status.", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key", + "connection": { "platform": "stripe" } + }, + "invoiceId": { + "type": "string", + "required": true, + "description": "The invoice ID (e.g., 'in_1abc...')" + }, + "expand": { + "type": "array", + "required": false, + "description": "Fields to expand (e.g., ['customer', 'subscription', 'charge'])" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query params with expand fields", + "type": "code", + "code": { + "source": "const { invoiceId, expand } = $.input;\nif (!invoiceId) throw new Error('invoiceId is required');\n\nconst params = {};\nif (expand && expand.length > 0) {\n expand.forEach((field, i) => {\n params[`expand[${i}]`] = field;\n });\n}\n\nreturn { params, invoiceId };" + } + }, + { + "id": "getInvoice", + "name": "Fetch the invoice from Stripe", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEgBiG8uU8::yzrlMIKqQkGSh7sxgkjl-g", + "connectionKey": "$.input.stripeConnectionKey", + "pathVars": { + "INVOICE": "$.steps.buildQuery.output.invoiceId" + }, + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "compressResult", + "name": "Strip verbose fields from the invoice", + "type": "code", + "code": { + "source": "const inv = $.steps.getInvoice.response || {};\n\nif (inv.error) {\n return { success: false, error: inv.error.message || 'Failed to retrieve invoice', details: inv.error };\n}\n\nconst keysToRemove = [\n 'object', 'account_tax_ids', 'amount_overpaid', 'application',\n 'application_fee_amount', 'attempt_count', 'attempted', 'auto_advance',\n 'automatically_finalizes_at', 'charge', 'confirmation_secret',\n 'custom_fields', 'customer_address', 'customer_phone', 'customer_shipping',\n 'customer_tax_exempt', 'customer_tax_ids', 'default_payment_method',\n 'default_source', 'default_tax_rates', 'discount', 'effective_at',\n 'ending_balance', 'footer', 'from_invoice', 'last_finalization_error',\n 'latest_revision', 'livemode', 'metadata', 'next_payment_attempt',\n 'on_behalf_of', 'parent', 'payment_settings', 'payments',\n 'post_payment_credit_notes_amount', 'pre_payment_credit_notes_amount',\n 'quote', 'receipt_number', 'rendering', 'shipping_cost', 'shipping_details',\n 'starting_balance', 'statement_descriptor', 'test_clock', 'threshold_reason',\n 'total_pretax_credit_amounts', 'webhooks_delivered_at'\n];\n\nconst lineKeysToRemove = ['object', 'livemode', 'metadata', 'parent', 'pretax_credit_amounts', 'pricing', 'taxes'];\n\nconst c = { ...inv };\nkeysToRemove.forEach(k => delete c[k]);\n\nif (c.lines && c.lines.data) {\n c.lines.data = c.lines.data.map(line => {\n const l = { ...line };\n lineKeysToRemove.forEach(k => delete l[k]);\n return l;\n });\n}\n\nreturn {\n success: true,\n invoice: c,\n summary: `Invoice ${c.id || inv.id}: ${c.status || 'unknown'} - ${(c.currency || 'usd').toUpperCase()} ${((c.amount_due || 0) / 100).toFixed(2)}`\n};" + } + } + ] +} diff --git a/flows/stripe/stripe-get-invoices.flow.json b/flows/stripe/stripe-get-invoices.flow.json new file mode 100644 index 0000000..2296f77 --- /dev/null +++ b/flows/stripe/stripe-get-invoices.flow.json @@ -0,0 +1,84 @@ +{ + "key": "stripe-get-invoices", + "name": "List Stripe Invoices", + "description": "Lists Stripe invoices with filtering by status, customer, date range, and collection method. Compresses the response by removing 30+ verbose fields per invoice while preserving all actionable data.", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key", + "connection": { "platform": "stripe" } + }, + "status": { + "type": "string", + "required": false, + "description": "Filter by status: 'draft', 'open', 'paid', 'uncollectible', or 'void'" + }, + "customer": { + "type": "string", + "required": false, + "description": "Filter by customer ID" + }, + "subscription": { + "type": "string", + "required": false, + "description": "Filter by subscription ID" + }, + "collectionMethod": { + "type": "string", + "required": false, + "description": "Filter by collection method: 'charge_automatically' or 'send_invoice'" + }, + "createdAfter": { + "type": "number", + "required": false, + "description": "Filter invoices created after this Unix timestamp" + }, + "createdBefore": { + "type": "number", + "required": false, + "description": "Filter invoices created before this Unix timestamp" + }, + "limit": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of invoices to return (1-100)" + }, + "startingAfter": { + "type": "string", + "required": false, + "description": "Cursor for pagination (invoice ID to start after)" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters with date filters", + "type": "code", + "code": { + "source": "const { status, customer, subscription, collectionMethod, createdAfter, createdBefore, limit, startingAfter } = $.input;\n\nconst params = {};\nif (status) params.status = status;\nif (customer) params.customer = customer;\nif (subscription) params.subscription = subscription;\nif (collectionMethod) params.collection_method = collectionMethod;\nif (createdAfter) params['created[gte]'] = String(createdAfter);\nif (createdBefore) params['created[lte]'] = String(createdBefore);\nif (limit) params.limit = String(Math.max(1, Math.min(100, limit)));\nif (startingAfter) params.starting_after = startingAfter;\n\nreturn { params };" + } + }, + { + "id": "listInvoices", + "name": "Fetch invoices from Stripe", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEgA9sdkqQ::GSd3xL1zTvSpx3JBvnVIJQ", + "connectionKey": "$.input.stripeConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "compressResults", + "name": "Strip verbose fields from each invoice and its line items", + "type": "code", + "code": { + "source": "const resp = $.steps.listInvoices.response || {};\nconst invoices = resp.data || [];\n\nconst invoiceKeysToRemove = [\n 'object', 'account_tax_ids', 'amount_overpaid', 'application',\n 'application_fee_amount', 'attempt_count', 'attempted', 'auto_advance',\n 'automatically_finalizes_at', 'charge', 'confirmation_secret',\n 'custom_fields', 'customer_address', 'customer_phone', 'customer_shipping',\n 'customer_tax_exempt', 'customer_tax_ids', 'default_payment_method',\n 'default_source', 'default_tax_rates', 'discount', 'effective_at',\n 'ending_balance', 'footer', 'from_invoice', 'last_finalization_error',\n 'latest_revision', 'livemode', 'metadata', 'next_payment_attempt',\n 'on_behalf_of', 'parent', 'payment_settings', 'payments',\n 'post_payment_credit_notes_amount', 'pre_payment_credit_notes_amount',\n 'quote', 'receipt_number', 'rendering', 'shipping_cost', 'shipping_details',\n 'starting_balance', 'statement_descriptor', 'test_clock', 'threshold_reason',\n 'total_pretax_credit_amounts', 'webhooks_delivered_at'\n];\n\nconst lineKeysToRemove = ['object', 'livemode', 'metadata', 'parent', 'pretax_credit_amounts', 'pricing', 'taxes'];\n\nfunction compressInvoice(inv) {\n const c = { ...inv };\n invoiceKeysToRemove.forEach(k => delete c[k]);\n\n if (c.lines && c.lines.data) {\n c.lines.data = c.lines.data.map(line => {\n const l = { ...line };\n lineKeysToRemove.forEach(k => delete l[k]);\n return l;\n });\n }\n\n return c;\n}\n\nconst compressed = invoices.map(compressInvoice);\n\nreturn {\n invoices: compressed,\n totalReturned: compressed.length,\n hasMore: resp.has_more || false,\n summary: `Retrieved ${compressed.length} invoice${compressed.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/stripe/stripe-get-payment-intent.flow.json b/flows/stripe/stripe-get-payment-intent.flow.json new file mode 100644 index 0000000..e9a08bf --- /dev/null +++ b/flows/stripe/stripe-get-payment-intent.flow.json @@ -0,0 +1,61 @@ +{ + "key": "stripe-get-payment-intent", + "name": "Get Stripe Payment Intent Detail", + "description": "Retrieves a single Stripe payment intent by ID. Compresses the response by removing internal fields while preserving amount, status, currency, customer, and payment method data.", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key", + "connection": { "platform": "stripe" } + }, + "intentId": { + "type": "string", + "required": true, + "description": "The payment intent ID (e.g., 'pi_1abc...')" + }, + "clientSecret": { + "type": "string", + "required": false, + "description": "Client secret for client-side retrieval" + }, + "expand": { + "type": "array", + "required": false, + "description": "Fields to expand (e.g., ['customer', 'payment_method', 'latest_charge'])" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query params", + "type": "code", + "code": { + "source": "const { intentId, clientSecret, expand } = $.input;\nif (!intentId) throw new Error('intentId is required');\n\nconst params = {};\nif (clientSecret) params.client_secret = clientSecret;\nif (expand && expand.length > 0) {\n expand.forEach((field, i) => {\n params[`expand[${i}]`] = field;\n });\n}\n\nreturn { params, intentId };" + } + }, + { + "id": "getPaymentIntent", + "name": "Fetch the payment intent from Stripe", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEgMHGJRig::7H_NNpAYQeeIFiENuhk-HA", + "connectionKey": "$.input.stripeConnectionKey", + "pathVars": { + "intent": "$.steps.buildQuery.output.intentId" + }, + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "compressResult", + "name": "Strip verbose internal fields", + "type": "code", + "code": { + "source": "const intent = $.steps.getPaymentIntent.response || {};\n\nif (intent.error) {\n return { success: false, error: intent.error.message || 'Failed to retrieve payment intent', details: intent.error };\n}\n\nconst keysToRemove = [\n 'object', 'amount_details', 'application', 'application_fee_amount',\n 'canceled_at', 'cancellation_reason', 'client_secret', 'confirmation_method',\n 'invoice', 'last_payment_error', 'latest_charge', 'livemode', 'next_action',\n 'on_behalf_of', 'payment_method_options', 'processing', 'receipt_email',\n 'review', 'setup_future_usage', 'shipping', 'source', 'statement_descriptor',\n 'statement_descriptor_suffix', 'transfer_data', 'transfer_group'\n];\n\nconst c = { ...intent };\nkeysToRemove.forEach(k => delete c[k]);\n\nconst amountStr = ((c.amount || 0) / 100).toFixed(2);\nconst currency = (c.currency || 'usd').toUpperCase();\n\nreturn {\n success: true,\n paymentIntent: c,\n summary: `Payment intent ${c.id}: ${c.status || 'unknown'} - ${currency} ${amountStr}`\n};" + } + } + ] +} diff --git a/flows/stripe/stripe-get-payment-intents.flow.json b/flows/stripe/stripe-get-payment-intents.flow.json new file mode 100644 index 0000000..6d4b319 --- /dev/null +++ b/flows/stripe/stripe-get-payment-intents.flow.json @@ -0,0 +1,69 @@ +{ + "key": "stripe-get-payment-intents", + "name": "List Stripe Payment Intents", + "description": "Lists Stripe payment intents with filtering by customer and date range. Compresses responses by removing internal fields (client_secret, payment_method_options, shipping, etc.).", + "version": "1", + "inputs": { + "stripeConnectionKey": { + "type": "string", + "required": true, + "description": "Stripe connection key", + "connection": { "platform": "stripe" } + }, + "customer": { + "type": "string", + "required": false, + "description": "Filter by customer ID" + }, + "createdAfter": { + "type": "number", + "required": false, + "description": "Filter payment intents created after this Unix timestamp" + }, + "createdBefore": { + "type": "number", + "required": false, + "description": "Filter payment intents created before this Unix timestamp" + }, + "limit": { + "type": "number", + "required": false, + "default": 10, + "description": "Number of payment intents to return (1-100)" + }, + "startingAfter": { + "type": "string", + "required": false, + "description": "Cursor for pagination (payment intent ID to start after)" + } + }, + "steps": [ + { + "id": "buildQuery", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const { customer, createdAfter, createdBefore, limit, startingAfter } = $.input;\n\nconst params = {};\nif (customer) params.customer = customer;\nif (createdAfter) params['created[gte]'] = String(createdAfter);\nif (createdBefore) params['created[lte]'] = String(createdBefore);\nif (limit) params.limit = String(Math.max(1, Math.min(100, limit)));\nif (startingAfter) params.starting_after = startingAfter;\n\nreturn { params };" + } + }, + { + "id": "listPaymentIntents", + "name": "Fetch payment intents from Stripe", + "type": "action", + "action": { + "platform": "stripe", + "actionId": "conn_mod_def::GHEgLeGEpAg::IWvl0S_nS_CUDugp36BGjQ", + "connectionKey": "$.input.stripeConnectionKey", + "queryParams": "$.steps.buildQuery.output.params" + } + }, + { + "id": "compressResults", + "name": "Strip verbose internal fields from each payment intent", + "type": "code", + "code": { + "source": "const resp = $.steps.listPaymentIntents.response || {};\nconst intents = resp.data || [];\n\nconst keysToRemove = [\n 'object', 'amount_details', 'application', 'application_fee_amount',\n 'canceled_at', 'cancellation_reason', 'client_secret', 'confirmation_method',\n 'invoice', 'last_payment_error', 'latest_charge', 'livemode', 'next_action',\n 'on_behalf_of', 'payment_method_options', 'processing', 'receipt_email',\n 'review', 'setup_future_usage', 'shipping', 'source', 'statement_descriptor',\n 'statement_descriptor_suffix', 'transfer_data', 'transfer_group'\n];\n\nconst compressed = intents.map(intent => {\n const c = { ...intent };\n keysToRemove.forEach(k => delete c[k]);\n return c;\n});\n\nreturn {\n paymentIntents: compressed,\n totalReturned: compressed.length,\n hasMore: resp.has_more || false,\n summary: `Retrieved ${compressed.length} payment intent${compressed.length === 1 ? '' : 's'}`\n};" + } + } + ] +} diff --git a/flows/trello/README.md b/flows/trello/README.md new file mode 100644 index 0000000..00aa66c --- /dev/null +++ b/flows/trello/README.md @@ -0,0 +1,91 @@ +--- +name: trello +description: | + Trello board management flow for the One CLI. Create cards, list cards, and + browse boards with automatic list name resolution. +triggers: + - "trello card" + - "create card" + - "trello board" + - "list cards" + - "/trello" +--- + +# Trello Flows + +Ready-to-run Trello board management via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add trello # Connect your Trello account +one --agent list # Find your connection key +``` + +## Discovery + +You need a `boardId` before using the flows. Find it with: + +```bash +# List all your Trello boards +one --agent actions search trello "get boards" +one --agent actions execute trello +``` + +The flow automatically resolves list names to IDs, so you only need the board ID. + +## Flows + +### Manage Cards + +Create a new card or list all cards on a board. Automatically fetches board lists +so you can reference lists by name instead of ID. + +**Create a card:** + +```bash +one flow execute trello-manage-cards.flow.json \ + --input trelloConnectionKey="" \ + --input boardId="" \ + --input action="create" \ + --input listName="To Do" \ + --input cardName="Review PR #42" \ + --input cardDesc="Check the API changes" +``` + +**List all cards on a board:** + +```bash +one flow execute trello-manage-cards.flow.json \ + --input trelloConnectionKey="" \ + --input boardId="" \ + --input action="list" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `trelloConnectionKey` | Yes | Your Trello connection key | +| `boardId` | Yes | Board ID (24-character hex) | +| `action` | Yes | `create` to add a card, `list` to get all cards | +| `listName` | No | List name for card creation (case-insensitive match) | +| `listId` | No | Direct list ID (overrides listName) | +| `cardName` | No | Card title (required for create) | +| `cardDesc` | No | Card description (Markdown supported) | +| `due` | No | Due date in ISO format | +| `pos` | No | Position: top, bottom, or number | + +**What it does under the hood:** + +1. Fetches all open lists on the board (so you can use list names, not just IDs) +2. For `create`: resolves list by name, creates card via Trello API +3. For `list`: fetches all open cards and maps each to its list name +4. Returns structured results with card URLs and list context + +## Adapting These Flows + +- **Move cards**: Fork the flow and add an update-card step with a new `idList` +- **Daily standup**: Chain `list` action with a Slack flow to post board status +- **Sprint board**: Create cards in bulk by looping the create action diff --git a/flows/trello/trello-manage-cards.flow.json b/flows/trello/trello-manage-cards.flow.json new file mode 100644 index 0000000..7e7f108 --- /dev/null +++ b/flows/trello/trello-manage-cards.flow.json @@ -0,0 +1,120 @@ +{ + "key": "trello-manage-cards", + "name": "Create and List Trello Cards", + "description": "Create a new card on a Trello board or list all cards on a board. Handles the common pattern of needing a list ID to create a card by first fetching lists from a board.", + "version": "1", + "inputs": { + "trelloConnectionKey": { + "type": "string", + "required": true, + "description": "Trello connection key", + "connection": { "platform": "trello" } + }, + "boardId": { + "type": "string", + "required": true, + "description": "Trello board ID (24-character hex string)" + }, + "action": { + "type": "string", + "required": true, + "description": "Action to perform: 'create' to add a card, 'list' to get all cards" + }, + "listName": { + "type": "string", + "required": false, + "description": "Name of the list to create the card in (used with action=create, matched case-insensitively)" + }, + "listId": { + "type": "string", + "required": false, + "description": "Direct list ID to create the card in (overrides listName)" + }, + "cardName": { + "type": "string", + "required": false, + "description": "Card name/title (required for action=create)" + }, + "cardDesc": { + "type": "string", + "required": false, + "description": "Card description (Markdown supported)" + }, + "due": { + "type": "string", + "required": false, + "description": "Due date in ISO format" + }, + "pos": { + "type": "string", + "required": false, + "description": "Position: top, bottom, or a positive number" + } + }, + "steps": [ + { + "id": "getLists", + "name": "Fetch board lists (needed to resolve list names)", + "type": "action", + "action": { + "platform": "trello", + "actionId": "conn_mod_def::GD17HL-ngQQ::Y25T18b0RBKdfZC0EfuMAQ", + "connectionKey": "$.input.trelloConnectionKey", + "pathVars": { + "id": "$.input.boardId" + }, + "queryParams": { + "filter": "open" + } + } + }, + { + "id": "resolveListAndRoute", + "name": "Resolve list ID and determine action", + "type": "code", + "code": { + "source": "const lists = $.steps.getLists.response || [];\nconst action = $.input.action;\nconst listName = $.input.listName;\nconst listId = $.input.listId;\n\nconst listMap = (Array.isArray(lists) ? lists : []).map(l => ({ id: l.id, name: l.name }));\n\nlet resolvedListId = listId;\nif (!resolvedListId && listName) {\n const match = listMap.find(l => l.name.toLowerCase() === listName.toLowerCase());\n if (match) resolvedListId = match.id;\n else throw new Error(`List \"${listName}\" not found. Available lists: ${listMap.map(l => l.name).join(', ')}`);\n}\n\nreturn { action, resolvedListId, listMap, isCreate: action === 'create' };" + } + }, + { + "id": "getCards", + "name": "Get all cards on the board", + "type": "action", + "if": "$.steps.resolveListAndRoute.output.action !== 'create'", + "action": { + "platform": "trello", + "actionId": "conn_mod_def::GD17G10GifA::ShEy6D00QdizAbzmsK3e-Q", + "connectionKey": "$.input.trelloConnectionKey", + "pathVars": { + "id": "$.input.boardId" + } + } + }, + { + "id": "createCard", + "name": "Create a new card", + "type": "action", + "if": "$.steps.resolveListAndRoute.output.action === 'create'", + "action": { + "platform": "trello", + "actionId": "conn_mod_def::GD17eEXfV5Y::KmRsMXHgRBKLphQif7Jrow", + "connectionKey": "$.input.trelloConnectionKey", + "queryParams": { + "idList": "$.steps.resolveListAndRoute.output.resolvedListId", + "name": "$.input.cardName", + "desc": "$.input.cardDesc", + "due": "$.input.due", + "pos": "$.input.pos" + } + } + }, + { + "id": "formatResult", + "name": "Format response", + "type": "code", + "code": { + "source": "const action = $.steps.resolveListAndRoute.output.action;\nconst listMap = $.steps.resolveListAndRoute.output.listMap;\n\nif (action === 'create') {\n const card = $.steps.createCard?.response || {};\n return {\n action: 'create',\n card: {\n id: card.id || '',\n name: card.name || '',\n url: card.shortUrl || card.url || '',\n list: listMap.find(l => l.id === card.idList)?.name || card.idList || '',\n due: card.due || null\n },\n summary: card.id ? `Created card \"${card.name}\" on list \"${listMap.find(l => l.id === card.idList)?.name || 'unknown'}\"` : 'Failed to create card'\n };\n} else {\n const cards = $.steps.getCards?.response || [];\n const formatted = (Array.isArray(cards) ? cards : []).map(c => ({\n id: c.id || '',\n name: c.name || '',\n list: listMap.find(l => l.id === c.idList)?.name || c.idList || '',\n url: c.shortUrl || c.url || '',\n due: c.due || null,\n closed: c.closed || false\n }));\n return {\n action: 'list',\n cards: formatted,\n totalCards: formatted.length,\n lists: listMap,\n summary: `Found ${formatted.length} card${formatted.length === 1 ? '' : 's'} across ${listMap.length} list${listMap.length === 1 ? '' : 's'}`\n };\n}" + } + } + ] +} diff --git a/flows/vercel/README.md b/flows/vercel/README.md new file mode 100644 index 0000000..cbcf1f9 --- /dev/null +++ b/flows/vercel/README.md @@ -0,0 +1,109 @@ +--- +name: vercel +description: | + Vercel deployment and project management flows for the One CLI. List projects, + create deployments, and view environment variables. +triggers: + - "deploy vercel" + - "vercel deploy" + - "vercel projects" + - "vercel env" + - "/vercel" +--- + +# Vercel Flows + +Ready-to-run deployment and project management via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add vercel # Connect your Vercel account +one --agent list # Find your connection key +``` + +## Discovery + +Some flows require a `projectId` or `teamId`. Find them using the list projects flow: + +```bash +# List all projects (returns project IDs and names) +one flow execute vercel-list-projects.flow.json \ + --input vercelConnectionKey="" + +# To find your team ID +one --agent actions search vercel "list teams" +one --agent actions execute vercel +``` + +## Flows + +### List Projects + +Lists all projects in your Vercel account or team. Supports filtering by name. + +```bash +one flow execute vercel-list-projects.flow.json \ + --input vercelConnectionKey="" \ + --input search="my-app" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `vercelConnectionKey` | Yes | Your Vercel connection key | +| `search` | No | Search projects by name | +| `teamId` | No | Team ID to list projects for | +| `limit` | No | Max number of projects to return | + +### Create Deployment + +Creates a new deployment on Vercel. Supports static file deploys, redeployments, +and team context. + +```bash +one flow execute vercel-create-deployment.flow.json \ + --input vercelConnectionKey="" \ + --input projectName="my-app" \ + --input target="production" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `vercelConnectionKey` | Yes | Your Vercel connection key | +| `projectName` | Yes | Project name for the deployment URL | +| `target` | No | Target: production, staging, or preview (default) | +| `teamId` | No | Team ID to deploy on behalf of | +| `deploymentId` | No | Previous deployment ID to redeploy | +| `files` | No | Array of file objects for static deploys | +| `framework` | No | Framework: nextjs, gatsby, nuxtjs, etc. | +| `buildCommand` | No | Custom build command | + +### View Environment Variables + +Retrieves environment variables for a project. Values are masked for security. + +```bash +one flow execute vercel-manage-env-vars.flow.json \ + --input vercelConnectionKey="" \ + --input projectId="my-app" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `vercelConnectionKey` | Yes | Your Vercel connection key | +| `projectId` | Yes | Project ID or name | +| `teamId` | No | Team ID if project belongs to a team | + +## Typical Workflow + +1. **List projects** to find the project you want to deploy +2. **Check env vars** to verify configuration is correct +3. **Create deployment** targeting production or preview +4. **Monitor** deployment status (check the returned URL) diff --git a/flows/vercel/vercel-create-deployment.flow.json b/flows/vercel/vercel-create-deployment.flow.json new file mode 100644 index 0000000..eddfa29 --- /dev/null +++ b/flows/vercel/vercel-create-deployment.flow.json @@ -0,0 +1,80 @@ +{ + "key": "vercel-create-deployment", + "name": "Create Vercel Deployment", + "description": "Create a new deployment on Vercel. Supports static file deployments, git-based deployments, and redeployments of previous deployments.", + "version": "1", + "inputs": { + "vercelConnectionKey": { + "type": "string", + "required": true, + "description": "Vercel connection key", + "connection": { "platform": "vercel" } + }, + "projectName": { + "type": "string", + "required": true, + "description": "Project name for the deployment URL" + }, + "target": { + "type": "string", + "required": false, + "default": "preview", + "description": "Deployment target: production, staging, or preview" + }, + "teamId": { + "type": "string", + "required": false, + "description": "Team ID to deploy on behalf of" + }, + "deploymentId": { + "type": "string", + "required": false, + "description": "Previous deployment ID to redeploy" + }, + "files": { + "type": "array", + "required": false, + "description": "Array of file objects: [{file: 'index.html', data: '...', encoding: 'utf-8'}]" + }, + "framework": { + "type": "string", + "required": false, + "description": "Framework: nextjs, gatsby, nuxtjs, etc." + }, + "buildCommand": { + "type": "string", + "required": false, + "description": "Custom build command" + } + }, + "steps": [ + { + "id": "buildRequest", + "name": "Build deployment request", + "type": "code", + "code": { + "source": "const { projectName, target, deploymentId, files, framework, buildCommand, teamId } = $.input;\nif (!projectName) throw new Error('projectName is required');\n\nconst body = { name: projectName };\nif (target && target !== 'preview') body.target = target;\nif (deploymentId) body.deploymentId = deploymentId;\nif (files && files.length) body.files = files;\n\nconst projectSettings = {};\nif (framework) projectSettings.framework = framework;\nif (buildCommand) projectSettings.buildCommand = buildCommand;\nif (Object.keys(projectSettings).length) body.projectSettings = projectSettings;\n\nconst params = {};\nif (teamId) params.teamId = teamId;\n\nreturn { body, params };" + } + }, + { + "id": "createDeployment", + "name": "Create Vercel deployment", + "type": "action", + "action": { + "platform": "vercel", + "actionId": "conn_mod_def::GIi152aO03U::KTvTF_0ZTaS3tObzavwy0g", + "connectionKey": "$.input.vercelConnectionKey", + "data": "$.steps.buildRequest.output.body", + "queryParams": "$.steps.buildRequest.output.params" + } + }, + { + "id": "formatResult", + "name": "Format deployment result", + "type": "code", + "code": { + "source": "const resp = $.steps.createDeployment.response || {};\nreturn {\n deploymentId: resp.id || '',\n url: resp.url || '',\n status: resp.status || resp.readyState || '',\n target: resp.target || 'preview',\n projectName: resp.name || $.input.projectName,\n aliases: resp.alias || [],\n regions: resp.regions || [],\n createdAt: resp.createdAt ? new Date(resp.createdAt).toISOString() : '',\n summary: resp.id ? `Deployment ${resp.id} created at ${resp.url || 'pending'} (${resp.status || resp.readyState || 'queued'})` : 'Failed to create deployment'\n};" + } + } + ] +} diff --git a/flows/vercel/vercel-list-projects.flow.json b/flows/vercel/vercel-list-projects.flow.json new file mode 100644 index 0000000..d4b7587 --- /dev/null +++ b/flows/vercel/vercel-list-projects.flow.json @@ -0,0 +1,58 @@ +{ + "key": "vercel-list-projects", + "name": "List Vercel Projects", + "description": "List all projects in your Vercel account or team. Supports filtering by name and pagination.", + "version": "1", + "inputs": { + "vercelConnectionKey": { + "type": "string", + "required": true, + "description": "Vercel connection key", + "connection": { "platform": "vercel" } + }, + "search": { + "type": "string", + "required": false, + "description": "Search projects by name" + }, + "teamId": { + "type": "string", + "required": false, + "description": "Team ID to list projects for" + }, + "limit": { + "type": "string", + "required": false, + "description": "Max number of projects to return" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const params = {};\nif ($.input.search) params.search = $.input.search;\nif ($.input.teamId) params.teamId = $.input.teamId;\nif ($.input.limit) params.limit = $.input.limit;\nreturn { params };" + } + }, + { + "id": "listProjects", + "name": "List Vercel projects", + "type": "action", + "action": { + "platform": "vercel", + "actionId": "conn_mod_def::GIi2eAZ5wsc::9YW3-ETsTJ6KD_Tg41KmtA", + "connectionKey": "$.input.vercelConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Format project list", + "type": "code", + "code": { + "source": "const resp = $.steps.listProjects.response || {};\nconst projects = (resp.projects || []).map(p => ({\n id: p.id || '',\n name: p.name || '',\n framework: p.framework || '',\n updatedAt: p.updatedAt ? new Date(p.updatedAt).toISOString() : '',\n paused: p.paused || false\n}));\n\nreturn {\n projects,\n totalProjects: projects.length,\n nextPage: resp.pagination?.next || null,\n summary: `Found ${projects.length} Vercel project${projects.length === 1 ? '' : 's'}${$.input.search ? ` matching \"${$.input.search}\"` : ''}`\n};" + } + } + ] +} diff --git a/flows/vercel/vercel-manage-env-vars.flow.json b/flows/vercel/vercel-manage-env-vars.flow.json new file mode 100644 index 0000000..ff0a3e5 --- /dev/null +++ b/flows/vercel/vercel-manage-env-vars.flow.json @@ -0,0 +1,56 @@ +{ + "key": "vercel-manage-env-vars", + "name": "View Vercel Project Environment Variables", + "description": "Retrieve environment variables for a Vercel project. Useful for auditing config before deployments.", + "version": "1", + "inputs": { + "vercelConnectionKey": { + "type": "string", + "required": true, + "description": "Vercel connection key", + "connection": { "platform": "vercel" } + }, + "projectId": { + "type": "string", + "required": true, + "description": "Project ID or name" + }, + "teamId": { + "type": "string", + "required": false, + "description": "Team ID (if project belongs to a team)" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build query parameters", + "type": "code", + "code": { + "source": "const params = {};\nif ($.input.teamId) params.teamId = $.input.teamId;\nreturn { params };" + } + }, + { + "id": "getEnvVars", + "name": "Retrieve environment variables", + "type": "action", + "action": { + "platform": "vercel", + "actionId": "conn_mod_def::GIi2iRBxUF4::H5rNeuVHSFqjc9IEVTiweA", + "connectionKey": "$.input.vercelConnectionKey", + "pathVars": { + "IDORNAME": "$.input.projectId" + }, + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Format env vars list", + "type": "code", + "code": { + "source": "const resp = $.steps.getEnvVars.response || {};\nconst envs = (resp.envs || resp || []);\nconst vars = (Array.isArray(envs) ? envs : []).map(e => ({\n key: e.key || '',\n value: e.value ? '***' : '',\n target: e.target || [],\n type: e.type || '',\n id: e.id || ''\n}));\n\nreturn {\n variables: vars,\n totalVars: vars.length,\n summary: `Found ${vars.length} environment variable${vars.length === 1 ? '' : 's'} for project \"${$.input.projectId}\"`\n};" + } + } + ] +} diff --git a/flows/zendesk/README.md b/flows/zendesk/README.md new file mode 100644 index 0000000..0d13422 --- /dev/null +++ b/flows/zendesk/README.md @@ -0,0 +1,79 @@ +--- +name: zendesk +description: | + Zendesk ticket management flows for the One CLI. Create tickets, search tickets, + and manage support workflows. +triggers: + - "create ticket" + - "zendesk ticket" + - "search tickets" + - "support ticket" + - "/zendesk" +--- + +# Zendesk Flows + +Ready-to-run support ticket management via the [One CLI](https://github.com/withoneai/one). + +## Setup + +```bash +npm i -g @withone/cli # Install One CLI +one add zendesk # Connect your Zendesk account +one --agent list # Find your connection key +``` + +## Flows + +### Create Ticket + +Creates a new support ticket. Supports plain text and HTML bodies, priority, +type, and tags. + +```bash +one flow execute zendesk-create-ticket.flow.json \ + --input zendeskConnectionKey="" \ + --input subject="Cannot login to dashboard" \ + --input body="User reports 403 error when accessing /dashboard after password reset." \ + --input priority="high" \ + --input tags='["login", "auth"]' +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `zendeskConnectionKey` | Yes | Your Zendesk connection key | +| `subject` | No | Ticket subject | +| `body` | Yes* | Ticket body (plain text) | +| `htmlBody` | No* | HTML body (overrides body for rich formatting) | +| `priority` | No | Priority: low, normal, high, urgent | +| `type` | No | Type: problem, incident, question, task | +| `tags` | No | Array of tags | + +*At least one of `body` or `htmlBody` is required. + +### Search Tickets + +Search for tickets with sorting and filtering options. + +```bash +one flow execute zendesk-search-tickets.flow.json \ + --input zendeskConnectionKey="" \ + --input sortOrder="desc" +``` + +**Inputs:** + +| Input | Required | Description | +|-------|----------|-------------| +| `zendeskConnectionKey` | Yes | Your Zendesk connection key | +| `sortOrder` | No | Sort: asc or desc (default: desc) | +| `externalId` | No | Filter by external ID | + +## Adapting These Flows + +- **Auto-triage**: Pipe incoming emails into `create-ticket` with tags based on content +- **SLA monitor**: Search tickets and filter by priority/age to find SLA breaches +- **Escalation**: Chain search with an update-ticket action to bump priority +- **Dashboard**: Combine search results with a Slack digest flow diff --git a/flows/zendesk/zendesk-create-ticket.flow.json b/flows/zendesk/zendesk-create-ticket.flow.json new file mode 100644 index 0000000..9eda02d --- /dev/null +++ b/flows/zendesk/zendesk-create-ticket.flow.json @@ -0,0 +1,73 @@ +{ + "key": "zendesk-create-ticket", + "name": "Create Zendesk Ticket", + "description": "Create a new support ticket in Zendesk. Supports subject, body (plain text or HTML), priority, tags, and custom fields.", + "version": "1", + "inputs": { + "zendeskConnectionKey": { + "type": "string", + "required": true, + "description": "Zendesk connection key", + "connection": { "platform": "zendesk" } + }, + "subject": { + "type": "string", + "required": false, + "description": "Ticket subject" + }, + "body": { + "type": "string", + "required": true, + "description": "Ticket description/comment body (plain text)" + }, + "htmlBody": { + "type": "string", + "required": false, + "description": "HTML body (overrides body if provided, use for rich formatting)" + }, + "priority": { + "type": "string", + "required": false, + "description": "Priority: low, normal, high, urgent" + }, + "type": { + "type": "string", + "required": false, + "description": "Ticket type: problem, incident, question, task" + }, + "tags": { + "type": "array", + "required": false, + "description": "Array of tags to apply to the ticket" + } + }, + "steps": [ + { + "id": "buildBody", + "name": "Build ticket creation payload", + "type": "code", + "code": { + "source": "const { subject, body, htmlBody, priority, type, tags } = $.input;\nif (!body && !htmlBody) throw new Error('body or htmlBody is required');\n\nconst comment = {};\nif (htmlBody) comment.html_body = htmlBody;\nelse comment.body = body;\n\nconst ticket = { comment };\nif (subject) ticket.subject = subject;\nif (priority) ticket.priority = priority;\nif (type) ticket.type = type;\nif (tags && tags.length) ticket.tags = tags;\n\nreturn { body: { ticket } };" + } + }, + { + "id": "createTicket", + "name": "Create ticket via Zendesk API", + "type": "action", + "action": { + "platform": "zendesk", + "actionId": "conn_mod_def::F6215yeST_g::ciFVh_4gQEeQhIGnAExeLQ", + "connectionKey": "$.input.zendeskConnectionKey", + "data": "$.steps.buildBody.output.body" + } + }, + { + "id": "formatResult", + "name": "Format ticket result", + "type": "code", + "code": { + "source": "const resp = $.steps.createTicket.response || {};\nconst ticket = resp.ticket || {};\nreturn {\n ticketId: ticket.id || '',\n subject: ticket.subject || $.input.subject || '',\n status: ticket.status || '',\n priority: ticket.priority || '',\n url: ticket.url || '',\n createdAt: ticket.created_at || '',\n tags: ticket.tags || [],\n summary: ticket.id ? `Created ticket #${ticket.id}: \"${ticket.subject || 'No subject'}\" (${ticket.status || 'open'})` : 'Failed to create ticket'\n};" + } + } + ] +} diff --git a/flows/zendesk/zendesk-search-tickets.flow.json b/flows/zendesk/zendesk-search-tickets.flow.json new file mode 100644 index 0000000..d4a0c8e --- /dev/null +++ b/flows/zendesk/zendesk-search-tickets.flow.json @@ -0,0 +1,54 @@ +{ + "key": "zendesk-search-tickets", + "name": "Search Zendesk Tickets", + "description": "Search for tickets in Zendesk. Returns ticket details including subject, status, priority, and assignee. Supports sorting and filtering by external ID.", + "version": "1", + "inputs": { + "zendeskConnectionKey": { + "type": "string", + "required": true, + "description": "Zendesk connection key", + "connection": { "platform": "zendesk" } + }, + "sortOrder": { + "type": "string", + "required": false, + "default": "desc", + "description": "Sort order: asc or desc" + }, + "externalId": { + "type": "string", + "required": false, + "description": "Filter by external ID" + } + }, + "steps": [ + { + "id": "buildParams", + "name": "Build search parameters", + "type": "code", + "code": { + "source": "const params = {};\nif ($.input.sortOrder) params.sort_order = $.input.sortOrder;\nif ($.input.externalId) params.external_id = $.input.externalId;\nreturn { params };" + } + }, + { + "id": "searchTickets", + "name": "Search tickets via Zendesk API", + "type": "action", + "action": { + "platform": "zendesk", + "actionId": "conn_mod_def::GABnnI_a8O4::DIJZSkogQ1iZMuULQLGjRQ", + "connectionKey": "$.input.zendeskConnectionKey", + "queryParams": "$.steps.buildParams.output.params" + } + }, + { + "id": "formatResult", + "name": "Format search results", + "type": "code", + "code": { + "source": "const resp = $.steps.searchTickets.response || {};\nconst tickets = (resp.tickets || []).map(t => ({\n id: t.id || '',\n subject: t.subject || '',\n status: t.status || '',\n priority: t.priority || '',\n type: t.type || '',\n createdAt: t.created_at || '',\n updatedAt: t.updated_at || '',\n tags: t.tags || [],\n assigneeId: t.assignee_id || null\n}));\n\nconst meta = resp.meta || {};\nreturn {\n tickets,\n totalTickets: tickets.length,\n hasMore: meta.has_more || false,\n afterCursor: meta.after_cursor || null,\n summary: `Found ${tickets.length} ticket${tickets.length === 1 ? '' : 's'}`\n};" + } + } + ] +}