From the department of things I wouldn't have bothered with a year ago, here's a python script to set Todoist label colors.
Why? I like a productivity environment with some color and flare, and it also helps to visually recognize what a task relates to. But setting label colors is more clicks than I have patience for.
How? Just figure out embeddings for each available color and then for each label. Use cosine similarity to set the color that best suits each label. Colors will stay consistent for existing labels and new ones will get just a dash of semantic meaning in their assignments.
Here's the code (you need an OpenAI API key and a Todoist API token set as environment variables):
import os
from todoist_api_python.api import TodoistAPI
from openai import OpenAI
import numpy as np
# Todoist palette colors from https://developer.todoist.com/api/v1#tag/Colors
colors = [
{"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"},
]
# OpenAI Client
oai = OpenAI()
# Embedding helper
def get_embedding(text):
# Replace underscores and hyphens with spaces
formatted_text = text.replace('_', ' ').replace('-', ' ')
response = oai.embeddings.create(
input=formatted_text,
model="text-embedding-3-small"
)
return response.data[0].embedding
# Add embeddings to each color in the colors list
print("Generating embeddings for colors...")
for color in colors:
color["embedding"] = get_embedding(color["name"])
# Get API token from environment variable for security
# You need to set this environment variable with your Todoist API token
api_token = os.environ.get("TODOIST_API_TOKEN")
if not api_token:
print("Error: TODOIST_API_TOKEN environment variable not set")
exit(1)
# Initialize the Todoist API client
api = TodoistAPI(api_token)
try:
# Get all labels
labels = api.get_labels()
for label_iter in labels:
for label in label_iter:
embedding = get_embedding(label.name)
# Fix the calculation of similarities - create a proper comparison array
color_embeddings = np.array([color["embedding"] for color in colors])
# Calculate similarities correctly - each embedding is a 1536-dim vector
similarities = np.array([np.dot(embedding, color_embedding) /
(np.linalg.norm(embedding) * np.linalg.norm(color_embedding))
for color_embedding in color_embeddings])
# Find the color with the highest similarity
max_index = np.argmax(similarities)
new_color = colors[max_index]["name"]
print(f"Setting {label.name} to {new_color}")
api.update_label(
label_id=label.id,
color=new_color
)
except Exception as error:
print(f"Error: {error}")
Updated 2026-03-07 03:28:
I have been wondering what a Cadillac version of this silly project would look like. The current version gives me colors, but not based on a lot of meaning. I also have to remember to find and run the python script when I add a new label. So I refactored this to run in Google Apps Script and to try and make those colors mean something. This is a two step process. First, the script uses GPT 5.4 to generate a description of each color including cultural significance and the kinds of tasks it might be associated with. Embeddings for these descriptions are cached. Second, the script loads three tasks for each label and finds the most similar color embedding for the sample tasks.
The script is scheduled to run weekly so I don't need to remember to do anything, always a big win.
It might end up being irritating as label colors will change over time. This might convey a subtle sense of what the label currently means, or it might just make it harder to remember the associations. Too soon to tell. In case this is ever helpful to anyone here's the code:
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;
}
Add Comment
All comments are moderated. Your email address is used to display a Gravatar and optionally for notification of new comments and to sign up for the newsletter.