Cycle's help center
LegalCompany
  • πŸš€Get started
    • πŸ€”What is Cycle? [Start here]
    • πŸ“₯Centralize feedback
    • πŸ’‘Extract insights from feedback
    • 🌱Document features in your roadmap
    • πŸ“‘Write your release notes and close the loop
  • πŸ’ͺCore concepts
    • 🏑Home
    • πŸ“₯Feedback
    • πŸ’‘Insights
    • ⛰️Roadmap
    • πŸ”ŠReleases
    • 🀩Closing the loop
    • ⚑Cycle AI
    • πŸ“ŠCustomer voice dashboards
  • πŸ₯³Latest Features
    • 🧠Custom Prompts
    • πŸ“‚Product Areas
    • πŸ“ŠCycle Dashboards
    • πŸ€–Cycle Ask – Your On‑Demand Product Analyst
  • βš™οΈSetup
    • πŸ”ŒIntegrations
      • Slack
      • Linear
      • Intercom
      • Zendesk
      • Salesforce
      • Gong
      • HubSpot
      • Zapier
      • Chrome extension
      • Figma, Loom, Miro & Pitch
      • Email
      • Notion
      • GitHub
      • Modjo
    • Product areas
    • πŸ“‹Views
    • 🏷️Properties
    • πŸ‘±Customers
    • 🎭Manage workspace members
    • πŸ””Notifications
    • πŸ”€Statuses
    • ✏️Collaborative, rich markdown and AI-powered editor
    • πŸ”“Security
  • 🚲Advanced
    • πŸŽ‰Public changelog
    • πŸ€–AI & automations
    • πŸ“ΉCall recording
    • πŸ“šContent templates
    • πŸ”οΈAdvanced Roadmaps and custom views
    • βš’οΈMultiple doc types
    • πŸ”Search
    • πŸ†Pro Cycler tips shortlist
    • ⌨️Keyboard shortcuts
  • πŸ”SAML/SSO
  • πŸ‘¨β€πŸ«Cycle University
    • πŸ‘οΈBest practices for views
    • 🏎️Fast-track your onboarding
    • 🐞Bug handling with Cycle
    • πŸ’»API
      • ⚑Webhook event for all properties changes
      • πŸ”—How to integrate with Jira Product Discovery
    • ❓FAQ
  • πŸ€”Guides
    • ⬇️Migrate your data to Cycle
      • Canny migration
    • πŸ“–Glossary
    • Synchronise your customer data
  • 🧱Core Documentation
    • πŸš€Capturing Feedback
    • ⚑Processing Feedback
    • πŸ“ŠAnalyzing Data
    • 🀩Closing the Loop
    • βš™οΈGetting Started / First Workspace Setup
  • πŸ“šGuides
    • πŸ”—Integrations
      • HubSpot
      • Salesforce
      • Linear
      • GitHub
      • Notion
      • Slack
      • Intercom
      • Zapier
      • Call recording
      • Email
      • Chrome extension
      • Gong
      • Zendesk
      • Modjo
    • πŸŒ†Views
    • πŸ“AI-powered editor
Powered by GitBook
On this page
  • Initial import
  • Real-time sync with your CRM
  • Syncing updates from CRM to Cycle
  • Daily synchronisation

Was this helpful?

  1. Guides

Synchronise your customer data

Cycle allow you to synchronise your customer data at company level.

Initial import

If you're just getting started, you’ll want to import your existing company and user data into Cycle. Here’s how to format your CSV for import:

companyName,name,email,customId,attioId,zendeskId,hubspotId,intercomId,pipedriveId,snowflakeId,salesforceId
Acme,John Doe,john@email.com,acme-001,a123,zendesk_456,hubspot_789,int_001,pipe_999,sf_acme_1,sf_001
Acme,Jane Doe,jame@email.com,acme-001,a123,zendesk_456,hubspot_789,int_001,pipe_999,sf_acme_1,sf_001
Wayne Enterprises,Bruce Wayne,bruce@wayne.com,wayne-001,a555,zendesk_789,hubspot_321,int_003,pipe_777,sf_wayne_1,sf_003
Airbnb,Main contact,main@airbnb.com,airbnb-001,a777,zendesk_000,hubspot_000,int_004,pipe_666,sf_airbnb_1,sf_004
Field name
Description

companyName

The name of the company

name

The full name of the person (Or `Main contact`)

email

Their email (or `main@company.com`)

customId (optional)

ID of your choice

attioId (optional)

ID of your Attio CRM

zendeskId (optional)

ID of your Zendesk CRM

hubspotId (optional)

ID of your HubSpot CRM

intercomId (optional)

ID of your Intercom CRM

pipedrive (optional)

ID of your Pipedrive CRM

snowflakeId (optional)

ID of your Snowflake CRM

Note that every row correspond to people granularity, so if you do not have any human contact but only a company, as the last row in the above example, use a Main contact approach.

IDs allow you later to match company via ID. So they are attached to Company and not people


Once your CSV is ready, you can upload it using Cycle’s GraphQL API.

graphqlCopyEditmutation importCustomersFromCSV($file: Upload!, $workspaceId: ID!) {
  importCustomersFromCSV(csvFile: $file, productId: $workspaceId)
}

Examples

curl https://api.product.cycle.app/graphql \
  -X POST \
  -H "Authorization: Bearer YOUR_CYCLE_API_KEY" \
  -F operations='{
    "query": "mutation importCustomersFromCSV($file: Upload!, $workspaceId: ID!) { importCustomersFromCSV(csvFile: $file, productId: $workspaceId) }",
    "variables": { "file": null, "workspaceId": "YOUR_WORKSPACE_ID" }
  }' \
  -F map='{ "0": ["variables.file"] }' \
  -F 0=@your_file.csv
import fs from 'fs';
import path from 'path';
import FormData from 'form-data';

const apiKey = 'YOUR_CYCLE_API_KEY';
const workspaceId = 'YOUR_WORKSPACE_ID';
const csvPath = './your_file.csv';

const form = new FormData();

form.append(
  'operations',
  JSON.stringify({
    query: `
      mutation importCustomersFromCSV($file: Upload!, $workspaceId: ID!) {
        importCustomersFromCSV(csvFile: $file, productId: $workspaceId)
      }
    `,
    variables: {
      file: null,
      workspaceId,
    },
  })
);

form.append('map', JSON.stringify({ '0': ['variables.file'] }));
form.append('0', fs.createReadStream(csvPath));

const response = await fetch('https://api.product.cycle.app/graphql', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    ...form.getHeaders(),
  },
  body: form,
});

const result = await response.json();
console.log(result);

import requests

# Replace with your actual values
API_KEY = 'YOUR_CYCLE_API_KEY'
WORKSPACE_ID = 'YOUR_WORKSPACE_ID'
CSV_PATH = 'your_file.csv'

url = 'https://api.product.cycle.app/graphql'

headers = {
    'Authorization': f'Bearer {API_KEY}'
}

operations = {
    "query": """
        mutation importCustomersFromCSV($file: Upload!, $workspaceId: ID!) {
            importCustomersFromCSV(csvFile: $file, productId: $workspaceId)
        }
    """,
    "variables": {
        "file": None,
        "workspaceId": WORKSPACE_ID
    }
}

map_ = {
    "0": ["variables.file"]
}

files = {
    'operations': (None, json.dumps(operations), 'application/json'),
    'map': (None, json.dumps(map_), 'application/json'),
    '0': (CSV_PATH, open(CSV_PATH, 'rb'), 'text/csv'),
}

response = requests.post(url, headers=headers, files=files)
print(response.status_code)
print(response.json())

Real-time sync with your CRM

Once your companies are in Cycle, you’ll want to keep them in sync with your CRM

For instance: When something changes in your CRM e.g. a company stage changes to β€œClosed Won”

Syncing updates from CRM to Cycle

Let’s say you just updated Acme’s stage to Closed Won in your CRM. You now want to reflect that change in Cycle.

To do that, you need to πŸ‘‡

Step 1: Fetch the attribute definition ID

Before updating any company attribute, you need to retrieve the definition ID of the attribute (e.g. Stage) πŸ‘‰ In this case we assume Stage is a Select value, so you will also need the value id

GraphQL query:

query workspaceBySlug($slug: DefaultString!) {
    getProductBySlug(slug: $slug) {
        id
        name
        slug
        companyAttributeDefinitions(
            pagination: {
                size: 100,
                where: {
                    cursor: "",
                    direction: AFTER
                }
            }
        ) {
            pageInfo {
                endCursor
                hasNextPage
            }
            edges {
                node {
                    __typename
                    ... on AttributeTextDefinition {
                        id
                        name
                    }
                    ... on AttributeNumberDefinition {
                        id
                        name
                    }
                    ... on AttributeCheckboxDefinition {
                        id
                        name
                    }
                    ... on AttributeSingleSelectDefinition {
                        id
                        name
                        valuesV2(pagination: {
                            size: 100,
                            where: {
                                cursor: "",
                                direction: AFTER
                            }
                        }) {
                            edges {
                                node {
                                    id
                                    value
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

This will return a list of all attribute definitions available in your workspace.

Find the one named Stage and copy its id (and the corresponding valueId if it's a select field).

Step 2: Update the company in Cycle

Once you have the attributeDefinitionId and (if needed) the valueId, use the following mutation:

updateCompanyAttributeValue(
  $companyId: ID!
  $attributeDefinitionId: ID!
  $value: CompanyAttributeValueInput!
) {
  updateCompanyAttributeValue(
    companyId: $companyId
    attributeDefinitionId: $attributeDefinitionId
    value: $value
  ) {
    __typename
    ... on CompanyAttributeCheckbox {
      id
      definition {
        id
        __typename
      }
      value {
        id
        valueCheckbox: value
      }
    }
    ... on CompanyAttributeText {
      id
      definition {
        id
        __typename
      }
      value {
        id
        valueText: value
      }
    }
    ... on CompanyAttributeSingleSelect {
      id
      definition {
        id
        __typename
      }
      value {
        id
        valueSelect: value
      }
    }
  }
}

Examples

curl https://api.product.cycle.app/graphql \
  -X POST \
  -H "Authorization: Bearer YOUR_CYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation updateCompanyAttributeValue($companyId: ID!, $attributeDefinitionId: ID!, $value: CompanyAttributeValueInput!) { updateCompanyAttributeValue(companyId: $companyId, attributeDefinitionId: $attributeDefinitionId, value: $value) { __typename ... on CompanyAttributeSingleSelect { id definition { id __typename } value { id valueSelect: value } } } }",
    "variables": {
      "companyId": "company-id-of-acme",
      "attributeDefinitionId": "definition-id-of-stage",
      "value": {
        "select": "value-id-for-closed-won"
      }
    }
  }'
const response = await fetch('https://api.product.cycle.app/graphql', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_CYCLE_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `
      mutation updateCompanyAttributeValue(
        $companyId: ID!, 
        $attributeDefinitionId: ID!, 
        $value: CompanyAttributeValueInput!
      ) {
        updateCompanyAttributeValue(
          companyId: $companyId,
          attributeDefinitionId: $attributeDefinitionId,
          value: $value
        ) {
          __typename
          ... on CompanyAttributeSingleSelect {
            id
            definition { id __typename }
            value { id valueSelect: value }
          }
        }
      }
    `,
    variables: {
      companyId: 'company-id-of-acme',
      attributeDefinitionId: 'definition-id-of-stage',
      value: { select: 'value-id-for-closed-won' },
    },
  }),
});

const result = await response.json();
console.log(result);
import requests
import json

url = 'https://api.product.cycle.app/graphql'
headers = {
    'Authorization': 'Bearer YOUR_CYCLE_API_KEY',
    'Content-Type': 'application/json'
}

query = """
mutation updateCompanyAttributeValue(
  $companyId: ID!, 
  $attributeDefinitionId: ID!, 
  $value: CompanyAttributeValueInput!
) {
  updateCompanyAttributeValue(
    companyId: $companyId,
    attributeDefinitionId: $attributeDefinitionId,
    value: $value
  ) {
    __typename
    ... on CompanyAttributeSingleSelect {
      id
      definition { id __typename }
      value { id valueSelect: value }
    }
  }
}
"""

variables = {
    "companyId": "company-id-of-acme",
    "attributeDefinitionId": "definition-id-of-stage",
    "value": {
        "select": "value-id-for-closed-won"
    }
}

response = requests.post(url, headers=headers, json={
    "query": query,
    "variables": variables
})

print(response.status_code)
print(response.json())

Create new attributes

If you need to add an attribute that does not exist yet, you first need to create it. So by using the following mutation, you are adding a "column" to your Company table so you will be able to filter on it in your dashboard.

curl https://api.product.cycle.app/graphql \
  -X POST \
  -H "Authorization: Bearer YOUR_CYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "
      mutation scalarTextAttribute($workspaceId: ID!, $name: String!) {
        addNewCompanyAttribute(
          input: {
            productId: $workspaceId
            name: $name
            color: \"a\"
            type: { scalar: { type: TEXT } }
          }
        ) {
          __typename
        }
      }
    ",
    "variables": {
      "workspaceId": "YOUR_WORKSPACE_ID",
      "name": "Churn reason"
    }
  }'
const createCompanyAttribute = async ({
  workspaceId,
  name,
  type,
  values = [], // Only needed for select type
}) => {
  let mutation = '';
  let variables = {};

  switch (type) {
    case 'NUMBER':
      mutation = `
        mutation createScalarAttribute($workspaceId: ID!, $name: DefaultString!) {
          addNewCompanyAttribute(
            input: {
              productId: $workspaceId
              name: $name
              color: "a"
              type: { scalar: { type: NUMBER } }
            }
          ) {
            __typename
          }
        }
      `;
      variables = {
        workspaceId,
        name,
      };
      break;

    case 'SINGLE_SELECT':
      if (values.length === 0) {
        throw new Error("Values are required for SINGLE_SELECT type");
      }
      mutation = `
        mutation createSingleSelectAttribute($workspaceId: ID!, $name: DefaultString!, $values: [SelectAttributeString!]!) {
          addNewCompanyAttribute(
            input: {
              productId: $workspaceId
              name: $name
              color: "a"
              type: { select: { type: SINGLE_SELECT, values: $values } }
            }
          ) {
            __typename
          }
        }
      `;
      variables = {
        workspaceId,
        name,
        values,
      };
      break;

    case 'TEXT':
      mutation = `
        mutation scalarTextAttribute($workspaceId: ID!, $name: DefaultString!) {
          addNewCompanyAttribute(
            input: {
              productId: $workspaceId
              name: $name
              color: "a"
              type: { scalar: { type: TEXT } }
            }
          ) {
            __typename
          }
        }
      `;
      variables = {
        workspaceId,
        name,
      };
      break;

    case 'BOOLEAN':
      mutation = `
        mutation scalarBooleanAttribute($workspaceId: ID!, $name: DefaultString!) {
          addNewCompanyAttribute(
            input: {
              productId: $workspaceId
              name: $name
              color: "a"
              type: { scalar: { type: BOOLEAN } }
            }
          ) {
            __typename
          }
        }
      `;
      variables = {
        workspaceId,
        name,
      };
      break;

    default:
      throw new Error("Invalid attribute type provided.");
  }

  try {
    const response = await fetch('https://api.product.cycle.app/graphql', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer YOUR_CYCLE_API_KEY`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: mutation,
        variables: variables,
      }),
    });

    const result = await response.json();
    return result.data.addNewCompanyAttribute || null;
  } catch (error) {
    console.error('Error creating company attribute:', error);
    throw error;
  }
};
import requests
import json

def create_company_attribute(workspace_id, name, attribute_type, values=None):
    mutation = ''
    variables = {}

    if attribute_type == 'NUMBER':
        mutation = """
        mutation createScalarAttribute($workspaceId: ID!, $name: String!) {
            addNewCompanyAttribute(
                input: {
                    productId: $workspaceId
                    name: $name
                    color: "a"
                    type: { scalar: { type: NUMBER } }
                }
            ) {
                __typename
            }
        }
        """
        variables = {
            "workspaceId": workspace_id,
            "name": name
        }

    elif attribute_type == 'SINGLE_SELECT':
        if not values:
            raise ValueError("Values are required for SINGLE_SELECT type")
        mutation = """
        mutation createSingleSelectAttribute($workspaceId: ID!, $name: String!, $values: [String!]!) {
            addNewCompanyAttribute(
                input: {
                    productId: $workspaceId
                    name: $name
                    color: "a"
                    type: { select: { type: SINGLE_SELECT, values: $values } }
                }
            ) {
                __typename
            }
        }
        """
        variables = {
            "workspaceId": workspace_id,
            "name": name,
            "values": values
        }

    elif attribute_type == 'TEXT':
        mutation = """
        mutation scalarTextAttribute($workspaceId: ID!, $name: String!) {
            addNewCompanyAttribute(
                input: {
                    productId: $workspaceId
                    name: $name
                    color: "a"
                    type: { scalar: { type: TEXT } }
                }
            ) {
                __typename
            }
        }
        """
        variables = {
            "workspaceId": workspace_id,
            "name": name
        }

    elif attribute_type == 'BOOLEAN':
        mutation = """
        mutation scalarBooleanAttribute($workspaceId: ID!, $name: String!) {
            addNewCompanyAttribute(
                input: {
                    productId: $workspaceId
                    name: $name
                    color: "a"
                    type: { scalar: { type: BOOLEAN } }
                }
            ) {
                __typename
            }
        }
        """
        variables = {
            "workspaceId": workspace_id,
            "name": name
        }

    else:
        raise ValueError("Invalid attribute type provided.")

    # Send the request to Cycle's API
    url = 'https://api.product.cycle.app/graphql'
    headers = {
        'Authorization': 'Bearer YOUR_CYCLE_API_KEY',
        'Content-Type': 'application/json'
    }

    payload = {
        'query': mutation,
        'variables': variables
    }

    try:
        response = requests.post(url, headers=headers, json=payload)
        response_data = response.json()

        if response.status_code == 200 and "data" in response_data:
            return response_data['data']['addNewCompanyAttribute']
        else:
            raise Exception(f"Failed to create attribute: {response_data.get('errors')}")
    except Exception as e:
        print(f"Error creating company attribute: {e}")
        return None

Get all existing attributes

The easiest way to get the current attributes is to fetch at the product level and use the ID you will retrieve.

curl https://api.product.cycle.app/graphql \
  -X POST \
  -H "Authorization: Bearer YOUR_CYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "
      query workspaceBySlug($slug: DefaultString!) {
        getProductBySlug(slug: $slug) {
          id
          name
          slug
          companyAttributeDefinitions(
            pagination: {
              size: 100,
              where: {
                cursor: \"\",
                direction: AFTER
              }
            }
          ) {
            pageInfo {
              endCursor
              hasNextPage
            }
            edges {
              node {
                __typename
                ... on AttributeTextDefinition {
                  id
                  name
                }
                ... on AttributeNumberDefinition {
                  id
                  name
                }
                ... on AttributeCheckboxDefinition {
                  id
                  name
                }
                ... on AttributeSingleSelectDefinition {
                  id
                  name
                  valuesV2(pagination: {
                    size: 100,
                    where: {
                      cursor: \"\",
                      direction: AFTER
                    }
                  }) {
                    edges {
                      node {
                        id
                        value
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    ",
    "variables": {
      "slug": "YOUR_WORKSPACE_SLUG"
    }
  }'

const fetchCompanyAttributes = async (slug) => {
  const query = `
    query workspaceBySlug($slug: DefaultString!) {
      getProductBySlug(slug: $slug) {
        id
        name
        slug
        companyAttributeDefinitions(
          pagination: {
            size: 100,
            where: {
              cursor: "",
              direction: AFTER
            }
          }
        ) {
          pageInfo {
            endCursor
            hasNextPage
          }
          edges {
            node {
              __typename
              ... on AttributeTextDefinition {
                id
                name
              }
              ... on AttributeNumberDefinition {
                id
                name
              }
              ... on AttributeCheckboxDefinition {
                id
                name
              }
              ... on AttributeSingleSelectDefinition {
                id
                name
                valuesV2(pagination: {
                  size: 100,
                  where: {
                    cursor: "",
                    direction: AFTER
                  }
                }) {
                  edges {
                    node {
                      id
                      value
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  `;
  
  const variables = {
    slug: slug,
  };

  const response = await fetch('https://api.product.cycle.app/graphql', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_CYCLE_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });

  const data = await response.json();
  return data.data.getProductBySlug.companyAttributeDefinitions.edges;
};
import requests
import json

def fetch_company_attributes(slug):
    query = """
    query workspaceBySlug($slug: DefaultString!) {
      getProductBySlug(slug: $slug) {
        id
        name
        slug
        companyAttributeDefinitions(
          pagination: {
            size: 100,
            where: {
              cursor: "",
              direction: AFTER
            }
          }
        ) {
          pageInfo {
            endCursor
            hasNextPage
          }
          edges {
            node {
              __typename
              ... on AttributeTextDefinition {
                id
                name
              }
              ... on AttributeNumberDefinition {
                id
                name
              }
              ... on AttributeCheckboxDefinition {
                id
                name
              }
              ... on AttributeSingleSelectDefinition {
                id
                name
                valuesV2(pagination: {
                  size: 100,
                  where: {
                    cursor: "",
                    direction: AFTER
                  }
                }) {
                  edges {
                    node {
                      id
                      value
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    """
    variables = {
        'slug': slug
    }

    headers = {
        'Authorization': 'Bearer YOUR_CYCLE_API_KEY',
        'Content-Type': 'application/json'
    }

    payload = {
        'query': query,
        'variables': variables
    }

    response = requests.post('https://api.product.cycle.app/graphql', headers=headers, json=payload)
    data = response.json()

    return data.get('data', {}).get('getProductBySlug', {}).get('companyAttributeDefinitions', {}).get('edges', [])

Daily synchronisation

A very typical way of keeping your data in-sync would be to have a cron job who run every day and update in bulk all your companies that changed during the last day.

  • Fetch companies from your database that were updated in the last 24 hours

  • Generate a CSV with those companies and the latest values

  • Use the mutation below to update them in bulk in Cycle

mutation updateCompanyAttributeValuesFromCSV($file: Upload!, $workspaceId: ID!) {
  updateCompanyAttributeValuesFromCSV(csvFile: $file, productId: $workspaceId)
}

⚠️ You must define which column to use as the unique company identifier during the update. It can be one of these: companyName, customId, attioId, zendeskId, hubspotId, intercomId, pipedriveId, snowflakeId, salesforceId

Cycle will use this column to match each row with an existing company in your workspace.

CSV format

Here’s an example of how your update CSV could look:

companyName	customId	attioId	zendeskId	hubspotId	intercomId	pipedriveId	snowflakeId	salesforceId	arr	numberOfEmployees	country	leadStatus	industry	closedDate	custom attribute
Acme			...			...			...			...			...			...			...			250000	50	France	Closed	HR	1678670383	...

Each column corresponds to an attribute in Cycle. You can update as many fields as you want in one go β€” just make sure the header names match exactly with the attribute names defined in your workspace (case-sensitive!).

Once your CSV is ready, upload it via the updateCompanyAttributeValuesFromCSV mutation.

PreviousGlossaryNextCapturing Feedback

Last updated 1 month ago

Was this helpful?

Here’s to use:

Note: Using this method mean that you will make an update for each single entity you have. Since we do have a in place you will be able to proceed with 2000 queries per hour otherwise you'll get rate-limited by our API.

πŸ€”
the mutation
rate-limit