Skip to content

Instantly share code, notes, and snippets.

@abfo
Last active March 7, 2026 03:19
Show Gist options
  • Select an option

  • Save abfo/08c0d295993fd1c2b3e4624d41024191 to your computer and use it in GitHub Desktop.

Select an option

Save abfo/08c0d295993fd1c2b3e4624d41024191 to your computer and use it in GitHub Desktop.
Set Todoist label colors from cosine similarity between a color description and tasks with the label. See https://ithoughthecamewithyou.com/post/set-todoist-label-colors-automatically-using-openai-embeddings
const TodoistApiToken = '';
const OpenAIApiKey = ''
const OpenAIResponseModel = 'gpt-5.4';
const OpenAIEmbeddingModel = 'text-embedding-3-small';
const TodoistColors = [
{"id": 30, "name": "berry_red", "hex": "#B8255F"},
{"id": 31, "name": "red", "hex": "#DC4C3E"},
{"id": 32, "name": "orange", "hex": "#C77100"},
{"id": 33, "name": "yellow", "hex": "#B29104"},
{"id": 34, "name": "olive_green", "hex": "#949C31"},
{"id": 35, "name": "lime_green", "hex": "#65A33A"},
{"id": 36, "name": "green", "hex": "#369307"},
{"id": 37, "name": "mint_green", "hex": "#42A393"},
{"id": 38, "name": "teal", "hex": "#148FAD"},
{"id": 39, "name": "sky_blue", "hex": "#319DC0"},
{"id": 40, "name": "light_blue", "hex": "#6988A4"},
{"id": 41, "name": "blue", "hex": "#4180FF"},
{"id": 42, "name": "grape", "hex": "#692EC2"},
{"id": 43, "name": "violet", "hex": "#CA3FEE"},
{"id": 44, "name": "lavender", "hex": "#A4698C"},
{"id": 45, "name": "magenta", "hex": "#E05095"},
{"id": 46, "name": "salmon", "hex": "#C9766F"},
{"id": 47, "name": "charcoal", "hex": "#808080"},
{"id": 48, "name": "grey", "hex": "#999999"},
{"id": 49, "name": "taupe", "hex": "#8F7A69"}
]
function updateLabels() {
const colorEmbeddings = loadEmbeddings(false);
const labels = getLabels();
for(const label of labels.results) {
let labelText = `${label.name}\n`;
const tasks = getTasks(label.name);
for(const task of tasks.results) {
labelText = labelText + `- ${task.content}`;
if (task.priority == 1) {
labelText = labelText + ' (High Priority)';
} else if (task.priority == 4) {
labelText = labelText + ' (Low Priority)';
}
labelText += '\n';
}
const labelEmbedding = generateEmbedding(labelText);
let highestSimilarity = -1.0;
let selectedColor = 47;
for (const colorEmbedding of colorEmbeddings) {
const similarity = cosineSimilarity(labelEmbedding, colorEmbedding.embedding);
if (similarity > highestSimilarity) {
highestSimilarity = similarity;
selectedColor = colorEmbedding.id;
}
}
updateLabelColor(label.id, selectedColor);
}
}
function generateEmbedding(input) {
const payload = {
model: OpenAIEmbeddingModel,
input: input
};
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': 'Bearer ' + OpenAIApiKey
},
payload: JSON.stringify(payload)
};
const response = UrlFetchApp.fetch('https://api.openai.com/v1/embeddings', options);
const result = JSON.parse(response.getContentText());
return result.data[0].embedding;
}
function forceEmbeddinbgs() {
loadEmbeddings(true);
}
function loadEmbeddings(force) {
const embeddings = [];
const props = PropertiesService.getUserProperties();
for (const color of TodoistColors) {
var saved = props.getProperty(color.name);
if (!saved || force) {
saved = JSON.stringify(embeddingForColor(color.name, color.hex));
props.setProperty(color.name, saved);
}
embeddings.push({'id': color.id, 'embedding': JSON.parse(saved)});
}
return embeddings;
}
function embeddingForColor(name, hex) {
const messages = [];
messages.push({
role: 'developer',
content: `You provide a paragraph descibing a color (the user will send you the name of the color and a specific hex color value).
The paragraph includes what the color might signify to a person in the United States in terms of emotions, activities, common uses and associations.
Describe the types of tasks on a to do list that might be associated with this color`
});
messages.push({
role: 'user',
content: 'Color Name: ' + name + ', Hex: ' + hex
});
const payload = {
model: OpenAIResponseModel,
messages: messages
};
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': 'Bearer ' + OpenAIApiKey
},
payload: JSON.stringify(payload)
};
const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', options);
const result = JSON.parse(response.getContentText());
const description = result.choices[0].message.content;
Logger.log(`Embedding for ${name} is ${description}.`)
return generateEmbedding(description);
}
function getLabels() {
var response = UrlFetchApp.fetch('https://api.todoist.com/api/v1/labels?limit=200', {
headers: {
Authorization: 'Bearer ' + TodoistApiToken
}
});
return JSON.parse(response.getContentText());
}
function updateLabelColor(labelId, colorId) {
const payload = {
color: colorId
};
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': 'Bearer ' + TodoistApiToken
},
payload: JSON.stringify(payload)
};
UrlFetchApp.fetch(`https://api.todoist.com/api/v1/labels/${labelId}`, options);
}
function getTasks(label) {
var response = UrlFetchApp.fetch(`https://api.todoist.com/api/v1/tasks?limit=3&label=${encodeURIComponent(label)}`, {
headers: {
Authorization: 'Bearer ' + TodoistApiToken
}
});
return JSON.parse(response.getContentText());
}
function cosineSimilarity(A, B) {
let dotproduct = 0;
let magnitudeA = 0;
let magnitudeB = 0;
for (let i = 0; i < A.length; i++) {
dotproduct += A[i] * B[i];
magnitudeA += A[i] * A[i];
magnitudeB += B[i] * B[i];
}
magnitudeA = Math.sqrt(magnitudeA);
magnitudeB = Math.sqrt(magnitudeB);
if (magnitudeA === 0 || magnitudeB === 0) {
return 0; // Cosine similarity is undefined for zero vectors, return 0
}
const similarity = dotproduct / (magnitudeA * magnitudeB);
return similarity;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment