Add to Candidatures (Notion)
Ce skill ajoute une candidature à la base Notion "Candidatures".
Méthode d'insertion — deux chemins selon le contexte
Chemin A — MCP Notion (prioritaire en session Cowork)
Utiliser l'outil notion-create-pages avec :
parent : { "type": "database_id", "database_id": "<NOTION_CANDIDATURES_DB_ID>" }
pages : tableau d'un objet { "properties": { ... } }
⚠️ Différences de nommage MCP vs API brute :
- Le champ "URL" s'appelle
userDefined:URL via le MCP (mais URL via l'API brute).
- Le MCP refuse les options
Poste inédites — utiliser impérativement une valeur de la liste ci-dessous.
Chemin B — Script Python (fallback si MCP non disponible)
Requiert NOTION_TOKEN dans l'environnement (voir section dédiée). Le script peut créer
des options Poste inédites car il passe par l'API brute.
Config / Credentials (Chemin B uniquement)
- DATABASE_ID = lu depuis
NOTION_CANDIDATURES_DB_ID (voir .env.example)
- TOKEN = intégration interne Notion, connectée à la base (menu "•••" → Connexions).
⚠️ Ne JAMAIS écrire le token en clair. Il est lu depuis la variable d'environnement
NOTION_TOKEN au moment de l'exécution.
- Endpoint : POST https://api.notion.com/v1/pages
- Headers : Authorization: Bearer · Notion-Version: 2022-06-28 · Content-Type: application/json
Définir le token (une fois, Chemin B)
- macOS/Linux (zsh/bash) : export NOTION_TOKEN="ta_cle" (permanent : ligne dans ~/.zshrc puis
source ~/.zshrc)
- Windows PowerShell : [Environment]::SetEnvironmentVariable("NOTION_TOKEN","ta_cle","User") (rouvrir le terminal)
- Windows cmd : setx NOTION_TOKEN "ta_cle" (rouvrir le terminal)
Règles globales
- Au démarrage, confirmer avec : 'Application du skill "Add to Notion Candidatures"'
- Ignorer toute entrée existante en corbeille (in_trash / archived).
- Envoyer les noms d'options de select/status EXACTEMENT tels qu'ils existent dans la
base (coquilles comprises) pour éviter les doublons.
- Un seul appel crée la candidature complète.
Schéma réel de la base
| Champ |
Type API |
Nom MCP |
Forme JSON (API brute) |
| Entreprise |
title |
Entreprise |
"title": [{ "text": { "content": }}] |
| Poste |
select |
Poste |
"select": { "name": } |
| Status |
status |
Status |
"status": { "name": } |
| Progress |
select |
Progress |
"select": { "name": } |
| Activité |
rich_text |
Activité |
"rich_text": [{ "text": { "content": }}] |
| URL |
rich_text |
userDefined:URL |
"rich_text": [{ "text": { "content": }}] <- texte |
| Via |
rich_text |
Via |
"rich_text": [{ "text": { "content": }}] |
| Fiche de poste |
url |
Fiche de poste |
"url": |
| Date |
rich_text |
Date |
"rich_text": [{ "text": { "content": }}] |
| Steps |
rich_text |
Steps |
laisser vide (ne pas inclure la clé) |
| Commentaires |
rich_text |
Commentaires |
"rich_text": [{ "text": { "content": }}] |
Valeurs par défaut / options fermées
- Status -> défaut "En cours".
Options RÉELLES : "En cours", "Success (non concluding)", "Success (offer)", "Archivée".
- Progress -> défaut "Postullé". ⚠️ Orthographe exacte (deux L) dans la base — envoyer tel quel.
Champ "Poste" — liste complète des options existantes
Choisir l'option la plus proche. Via MCP (Chemin A), toute valeur hors liste génère une erreur.
Via API brute (Chemin B), une valeur inédite est créée automatiquement.
"Senior Fullstack Engineer", "Lead Dev / CTO", "Lead JS Engineer", "Backend",
"Principal Frontend", "Software Architect", "Senior JS Engineer", "Tech Lead",
"Staff Software Engineer", "Senior Frontend Engineer", "Lead Frontend Engineer",
"Senior/Lead Dev", "Lead Backend Engineer", "Fouding Engineer",
"Staff/Lead Software Engineer", "Senior Software Engineer", "Enginering Lead Manager",
"Senior/Staff/Principal Software Engineer", "Lead Dev", "First Engineer",
"Staff/Senior Frontend Engineer", "Full Stack Engineer", "Senior/Staff Software Engineer",
"Fullstack Product Engineer", "Senior Fullstack Architect & AI Enabler",
"Staff Fullstack Engineer", "Founding Software Engineer", "Principal Fullstack Engineer",
"Senior Product Engineer", "Fullstack Software Engineer", "Frontend Engineer",
"Senior/Lead Fullstack Developer", "Senior Backend Engineer", "Software Engineer",
"Staff Frontend Engineer", "Senior Full Stack Engineer", "Software Engineer III Frontend",
"Staff Engineer", "Lead Fullstack Engineer", "Head of Engineering",
"Software Engineer (Integrations)", "Senior Software Developer", "Junior Full-Stack Engineer",
"Senior Platform Engineer", "Team Lead", "Frontend Developer (React)",
"Développeur Front-end", "Front End Tech Lead", "Product Engineer AI",
"Senior Full-Stack Software Engineer", "Senior Next.js Developer",
"Senior Customer Engineer", "Engineering Manager", "Software Engineer Platform",
"Fullstack Engineer"
Champ "Entreprise" (titre)
- Détecter le nom dans la fiche. Tiers recruteur sans client nommé -> "??? via " + nom du tiers.
- Non précisé mais devinable -> tenter ; si incertain -> "???". Si deviné, l'indiquer dans "Commentaires".
Champ "Activité" (texte)
- Déduire l'activité depuis la fiche et/ou le site, en s'inspirant des valeurs existantes.
- Tiers sans client nommé et non devinable -> laisser vide.
Champ "URL" (texte — site entreprise)
- Site officiel via recherche web simple. Vide si entreprise non définie/devinée.
- Ne JAMAIS mettre l'URL du dépositaire tiers.
Champ "Via" (texte)
- Entreprise nommée (par l'annonceur OU identifiée par recherche externe) -> nom du job board.
- Tiers pour entreprise non identifiée -> "NomDuTiers via NomDuJobBoard".
- Job boards : LinkedIn, WTTJ (Welcome to the Jungle), Collective.work, Jobgether, BAO...
Champ "Fiche de poste" (url)
- URL de l'annonce, nettoyée des paramètres de tracking.
Champ "Date" (texte)
- Date du jour, format "DD mois AAAA", mois FR minuscule (ex : 06 juillet 2026). Pas un date-picker.
Champ "Steps" — laisser vide (ne pas inclure la clé).
Champ "Commentaires" (texte)
- Fourchette salariale en K€ (ou TJM en €/j pour freelance) si précisée (sinon "non précisée").
- Rythme de télétravail si précisé. Toute déduction faite sur l'entreprise.
Flux d'exécution
Lire la fiche de poste.
- Utiliser
mcp__claude-in-chrome__get_page_text sur l'onglet actif.
- Si le contenu retourné est vide ou minimal (page LinkedIn sans session, SPA non rendue) :
s'appuyer sur les informations disponibles dans le titre de page et l'URL, sans tenter de
navigation supplémentaire vers la plateforme d'origine.
Identifier l'entreprise cliente.
- Si l'annonceur est un job board / plateforme (Free-Work, WTTJ, Jobgether, etc.) ET que le
nom du client final n'apparaît nulle part dans le texte extrait -> poser directement
Entreprise = "??? via <NomPlateforme>" sans lancer de recherche web.
- Si l'entreprise est nommée ou clairement devinable -> recherche web pour le site officiel.
Choisir le champ "Poste" parmi la liste des options existantes (section ci-dessus).
Créer l'entrée :
- Chemin A (Cowork) : appeler
notion-create-pages avec database_id.
- Chemin B (fallback) : lancer le script Python ci-dessous.
Vérifier la réponse (id + url). En cas d'erreur d'option select -> corriger et relancer.
Notes / pièges
- Coquilles à respecter : "Postullé" (Progress) ; "Fouding Engineer", "Enginering Lead Manager" (Poste).
- "URL" est du texte (rich_text), pas un type url. Via MCP, son nom est "userDefined:URL".
- "Status" est de type
status, pas select.
- LinkedIn sans session connectée retourne un contenu minimal (titre + salaire + lieu).
C'est suffisant pour remplir la fiche — ne pas tenter de naviguer vers Free-Work ou autre
plateforme pour retrouver l'annonce originale.
Script — add_candidature.py (Chemin B — portable Mac/PC, stdlib uniquement)
Utilisation
python add_candidature.py --entreprise "Fabulous" --poste "Senior Backend Engineer" \
--date "06 juillet 2026" --via "LinkedIn" \
--activite "App de bien-être & coaching self-care" \
--url "https://www.thefabulous.co" \
--fiche "https://www.linkedin.com/jobs/view/4413418313/" \
--commentaires "Full remote, Paris. Salaire non précisé."
(Sur macOS, utiliser python3 si python n'est pas trouvé.)
#!/usr/bin/env python3
"""add_candidature.py — Ajoute une candidature à la base Notion "Candidatures"."""
import os
import sys
import json
import argparse
import urllib.request
import urllib.error
DATABASE_ID = os.environ.get("NOTION_CANDIDATURES_DB_ID")
NOTION_VERSION = "2022-06-28"
def rich(txt):
return {"rich_text": [{"text": {"content": txt}}]} if txt else {"rich_text": []}
def main():
token = os.environ.get("NOTION_TOKEN")
if not token:
sys.exit("Erreur : variable d'environnement NOTION_TOKEN non définie.")
if not DATABASE_ID:
sys.exit("Erreur : variable d'environnement NOTION_CANDIDATURES_DB_ID non définie.")
p = argparse.ArgumentParser()
p.add_argument("--entreprise", required=True)
p.add_argument("--poste", required=True)
p.add_argument("--date", required=True, help='Format "DD mois AAAA", ex: 06 juillet 2026')
p.add_argument("--via", default="")
p.add_argument("--activite", default="")
p.add_argument("--url", default="") # site entreprise (champ texte)
p.add_argument("--fiche", default="") # URL annonce (champ url)
p.add_argument("--commentaires", default="")
p.add_argument("--status", default="En cours")
p.add_argument("--progress", default="Postullé") # orthographe exacte de la base
args = p.parse_args()
props = {
"Entreprise": {"title": [{"text": {"content": args.entreprise}}]},
"Poste": {"select": {"name": args.poste}},
"Status": {"status": {"name": args.status}},
"Progress": {"select": {"name": args.progress}},
"Activité": rich(args.activite),
"URL": rich(args.url),
"Via": rich(args.via),
"Date": rich(args.date),
"Commentaires": rich(args.commentaires),
}
if args.fiche:
props["Fiche de poste"] = {"url": args.fiche}
body = json.dumps({
"parent": {"database_id": DATABASE_ID},
"properties": props,
}).encode("utf-8")
req = urllib.request.Request(
"https://api.notion.com/v1/pages",
data=body,
method="POST",
headers={
"Authorization": f"Bearer {token}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req) as resp:
data = json.load(resp)
print(f"OK — candidature créée : {data.get('id')}")
print(f"URL : {data.get('url')}")
except urllib.error.HTTPError as e:
print(f"Erreur {e.code} : {e.read().decode('utf-8')}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
1---2name: add-to-candidatures3description: Ajouter une entrée à la base Notion "Candidatures". Utiliser ce skill dès que l'utilisateur mentionne vouloir sauvegarder, ajouter, enregistrer ou archiver une fiche de poste à laquelle il a candidaté, une URL, un lien ou un contenu dans Notion. Également déclencher si l'utilisateur dit "ajoute aux candidatures", ou toute formulation similaire.4---56# Add to Candidatures (Notion)78Ce skill ajoute une candidature à la base Notion "Candidatures".910## Méthode d'insertion — deux chemins selon le contexte1112### Chemin A — MCP Notion (prioritaire en session Cowork)13Utiliser l'outil `notion-create-pages` avec :14- `parent` : `{ "type": "database_id", "database_id": "<NOTION_CANDIDATURES_DB_ID>" }`15- `pages` : tableau d'un objet `{ "properties": { ... } }`1617⚠️ Différences de nommage MCP vs API brute :18- Le champ "URL" s'appelle **`userDefined:URL`** via le MCP (mais `URL` via l'API brute).19- Le MCP **refuse les options `Poste` inédites** — utiliser impérativement une valeur de la liste ci-dessous.2021### Chemin B — Script Python (fallback si MCP non disponible)22Requiert `NOTION_TOKEN` dans l'environnement (voir section dédiée). Le script peut créer23des options `Poste` inédites car il passe par l'API brute.2425## Config / Credentials (Chemin B uniquement)26- DATABASE_ID = lu depuis `NOTION_CANDIDATURES_DB_ID` (voir `.env.example`)27- TOKEN = intégration interne Notion, connectée à la base (menu "•••" → Connexions).28 ⚠️ Ne JAMAIS écrire le token en clair. Il est lu depuis la variable d'environnement29 NOTION_TOKEN au moment de l'exécution.30- Endpoint : POST https://api.notion.com/v1/pages31- Headers : Authorization: Bearer <TOKEN> · Notion-Version: 2022-06-28 · Content-Type: application/json3233## Définir le token (une fois, Chemin B)34- macOS/Linux (zsh/bash) : export NOTION_TOKEN="ta_cle" (permanent : ligne dans ~/.zshrc puis `source ~/.zshrc`)35- Windows PowerShell : [Environment]::SetEnvironmentVariable("NOTION_TOKEN","ta_cle","User") (rouvrir le terminal)36- Windows cmd : setx NOTION_TOKEN "ta_cle" (rouvrir le terminal)3738## Règles globales39- Au démarrage, confirmer avec : 'Application du skill "Add to Notion Candidatures"'40- Ignorer toute entrée existante en corbeille (in_trash / archived).41- Envoyer les noms d'options de select/status EXACTEMENT tels qu'ils existent dans la42 base (coquilles comprises) pour éviter les doublons.43- Un seul appel crée la candidature complète.4445## Schéma réel de la base46| Champ | Type API | Nom MCP | Forme JSON (API brute) |47|-----------------|------------|---------------------|------------------------------------------------------------|48| Entreprise | title | Entreprise | "title": [{ "text": { "content": <str> }}] |49| Poste | select | Poste | "select": { "name": <str> } |50| Status | status | Status | "status": { "name": <str> } |51| Progress | select | Progress | "select": { "name": <str> } |52| Activité | rich_text | Activité | "rich_text": [{ "text": { "content": <str> }}] |53| URL | rich_text | **userDefined:URL** | "rich_text": [{ "text": { "content": <str> }}] <- texte |54| Via | rich_text | Via | "rich_text": [{ "text": { "content": <str> }}] |55| Fiche de poste | url | Fiche de poste | "url": <str> |56| Date | rich_text | Date | "rich_text": [{ "text": { "content": <str> }}] |57| Steps | rich_text | Steps | laisser vide (ne pas inclure la clé) |58| Commentaires | rich_text | Commentaires | "rich_text": [{ "text": { "content": <str> }}] |5960## Valeurs par défaut / options fermées61- Status -> défaut "En cours".62 Options RÉELLES : "En cours", "Success (non concluding)", "Success (offer)", "Archivée".63- Progress -> défaut "Postullé". ⚠️ Orthographe exacte (deux L) dans la base — envoyer tel quel.6465## Champ "Poste" — liste complète des options existantes66Choisir l'option la plus proche. Via MCP (Chemin A), toute valeur hors liste génère une erreur.67Via API brute (Chemin B), une valeur inédite est créée automatiquement.6869"Senior Fullstack Engineer", "Lead Dev / CTO", "Lead JS Engineer", "Backend",70"Principal Frontend", "Software Architect", "Senior JS Engineer", "Tech Lead",71"Staff Software Engineer", "Senior Frontend Engineer", "Lead Frontend Engineer",72"Senior/Lead Dev", "Lead Backend Engineer", "Fouding Engineer",73"Staff/Lead Software Engineer", "Senior Software Engineer", "Enginering Lead Manager",74"Senior/Staff/Principal Software Engineer", "Lead Dev", "First Engineer",75"Staff/Senior Frontend Engineer", "Full Stack Engineer", "Senior/Staff Software Engineer",76"Fullstack Product Engineer", "Senior Fullstack Architect & AI Enabler",77"Staff Fullstack Engineer", "Founding Software Engineer", "Principal Fullstack Engineer",78"Senior Product Engineer", "Fullstack Software Engineer", "Frontend Engineer",79"Senior/Lead Fullstack Developer", "Senior Backend Engineer", "Software Engineer",80"Staff Frontend Engineer", "Senior Full Stack Engineer", "Software Engineer III Frontend",81"Staff Engineer", "Lead Fullstack Engineer", "Head of Engineering",82"Software Engineer (Integrations)", "Senior Software Developer", "Junior Full-Stack Engineer",83"Senior Platform Engineer", "Team Lead", "Frontend Developer (React)",84"Développeur Front-end", "Front End Tech Lead", "Product Engineer AI",85"Senior Full-Stack Software Engineer", "Senior Next.js Developer",86"Senior Customer Engineer", "Engineering Manager", "Software Engineer Platform",87"Fullstack Engineer"8889## Champ "Entreprise" (titre)90- Détecter le nom dans la fiche. Tiers recruteur sans client nommé -> "??? via " + nom du tiers.91- Non précisé mais devinable -> tenter ; si incertain -> "???". Si deviné, l'indiquer dans "Commentaires".9293## Champ "Activité" (texte)94- Déduire l'activité depuis la fiche et/ou le site, en s'inspirant des valeurs existantes.95- Tiers sans client nommé et non devinable -> laisser vide.9697## Champ "URL" (texte — site entreprise)98- Site officiel via recherche web simple. Vide si entreprise non définie/devinée.99- Ne JAMAIS mettre l'URL du dépositaire tiers.100101## Champ "Via" (texte)102- Entreprise nommée (par l'annonceur OU identifiée par recherche externe) -> nom du job board.103- Tiers pour entreprise non identifiée -> "NomDuTiers via NomDuJobBoard".104- Job boards : LinkedIn, WTTJ (Welcome to the Jungle), Collective.work, Jobgether, BAO...105106## Champ "Fiche de poste" (url)107- URL de l'annonce, nettoyée des paramètres de tracking.108 - LinkedIn : garder uniquement https://www.linkedin.com/jobs/view/<ID>/109110## Champ "Date" (texte)111- Date du jour, format "DD mois AAAA", mois FR minuscule (ex : 06 juillet 2026). Pas un date-picker.112113## Champ "Steps" — laisser vide (ne pas inclure la clé).114115## Champ "Commentaires" (texte)116- Fourchette salariale en K€ (ou TJM en €/j pour freelance) si précisée (sinon "non précisée").117- Rythme de télétravail si précisé. Toute déduction faite sur l'entreprise.118119## Flux d'exécution1201. **Lire la fiche de poste.**121 - Utiliser `mcp__claude-in-chrome__get_page_text` sur l'onglet actif.122 - Si le contenu retourné est vide ou minimal (page LinkedIn sans session, SPA non rendue) :123 s'appuyer sur les informations disponibles dans le titre de page et l'URL, sans tenter de124 navigation supplémentaire vers la plateforme d'origine.1251262. **Identifier l'entreprise cliente.**127 - Si l'annonceur est un job board / plateforme (Free-Work, WTTJ, Jobgether, etc.) ET que le128 nom du client final n'apparaît nulle part dans le texte extrait -> poser directement129 `Entreprise = "??? via <NomPlateforme>"` **sans lancer de recherche web**.130 - Si l'entreprise est nommée ou clairement devinable -> recherche web pour le site officiel.1311323. **Choisir le champ "Poste"** parmi la liste des options existantes (section ci-dessus).1331344. **Créer l'entrée** :135 - Chemin A (Cowork) : appeler `notion-create-pages` avec `database_id`.136 - Chemin B (fallback) : lancer le script Python ci-dessous.1371385. Vérifier la réponse (id + url). En cas d'erreur d'option select -> corriger et relancer.139140## Notes / pièges141- Coquilles à respecter : "Postullé" (Progress) ; "Fouding Engineer", "Enginering Lead Manager" (Poste).142- "URL" est du texte (rich_text), pas un type url. Via MCP, son nom est "userDefined:URL".143- "Status" est de type `status`, pas `select`.144- LinkedIn sans session connectée retourne un contenu minimal (titre + salaire + lieu).145 C'est suffisant pour remplir la fiche — ne pas tenter de naviguer vers Free-Work ou autre146 plateforme pour retrouver l'annonce originale.147148## Script — add_candidature.py (Chemin B — portable Mac/PC, stdlib uniquement)149150### Utilisation151 python add_candidature.py --entreprise "Fabulous" --poste "Senior Backend Engineer" \152 --date "06 juillet 2026" --via "LinkedIn" \153 --activite "App de bien-être & coaching self-care" \154 --url "https://www.thefabulous.co" \155 --fiche "https://www.linkedin.com/jobs/view/4413418313/" \156 --commentaires "Full remote, Paris. Salaire non précisé."157 (Sur macOS, utiliser `python3` si `python` n'est pas trouvé.)158159```python160#!/usr/bin/env python3161"""add_candidature.py — Ajoute une candidature à la base Notion "Candidatures"."""162import os163import sys164import json165import argparse166import urllib.request167import urllib.error168169DATABASE_ID = os.environ.get("NOTION_CANDIDATURES_DB_ID")170NOTION_VERSION = "2022-06-28"171172def rich(txt):173 return {"rich_text": [{"text": {"content": txt}}]} if txt else {"rich_text": []}174175def main():176 token = os.environ.get("NOTION_TOKEN")177 if not token:178 sys.exit("Erreur : variable d'environnement NOTION_TOKEN non définie.")179 if not DATABASE_ID:180 sys.exit("Erreur : variable d'environnement NOTION_CANDIDATURES_DB_ID non définie.")181182 p = argparse.ArgumentParser()183 p.add_argument("--entreprise", required=True)184 p.add_argument("--poste", required=True)185 p.add_argument("--date", required=True, help='Format "DD mois AAAA", ex: 06 juillet 2026')186 p.add_argument("--via", default="")187 p.add_argument("--activite", default="")188 p.add_argument("--url", default="") # site entreprise (champ texte)189 p.add_argument("--fiche", default="") # URL annonce (champ url)190 p.add_argument("--commentaires", default="")191 p.add_argument("--status", default="En cours")192 p.add_argument("--progress", default="Postullé") # orthographe exacte de la base193 args = p.parse_args()194195 props = {196 "Entreprise": {"title": [{"text": {"content": args.entreprise}}]},197 "Poste": {"select": {"name": args.poste}},198 "Status": {"status": {"name": args.status}},199 "Progress": {"select": {"name": args.progress}},200 "Activité": rich(args.activite),201 "URL": rich(args.url),202 "Via": rich(args.via),203 "Date": rich(args.date),204 "Commentaires": rich(args.commentaires),205 }206 if args.fiche:207 props["Fiche de poste"] = {"url": args.fiche}208209 body = json.dumps({210 "parent": {"database_id": DATABASE_ID},211 "properties": props,212 }).encode("utf-8")213214 req = urllib.request.Request(215 "https://api.notion.com/v1/pages",216 data=body,217 method="POST",218 headers={219 "Authorization": f"Bearer {token}",220 "Notion-Version": NOTION_VERSION,221 "Content-Type": "application/json",222 },223 )224 try:225 with urllib.request.urlopen(req) as resp:226 data = json.load(resp)227 print(f"OK — candidature créée : {data.get('id')}")228 print(f"URL : {data.get('url')}")229 except urllib.error.HTTPError as e:230 print(f"Erreur {e.code} : {e.read().decode('utf-8')}", file=sys.stderr)231 sys.exit(1)232233if __name__ == "__main__":234 main()235```