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
companyName
The name of the company
name
The full name of the person (Or `Main contact`)
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.
Last updated
Was this helpful?