Version originale (en anglais) Hundreds of projects, zero manual staffing: AI Studio + Script Actions
Bonjour à tous ![]()
Je suis Arthur, Forum Leader et passionné par l’IA et les automatisations dans Asana. N’hésitez pas à me donner votre avis sur ce cas d’usage ; je serai ravi de répondre à vos questions. ![]()
Quand Asana conjugue automatisation et passage à l’échelle
De nombreuses équipes sont confrontées à la même problématique : un flux constant de demandes similaires (mises à jour logicielles, campagnes, onboardings, changements de processus, etc.). Chacune d’entre elles nécessite la création d’un projet structuré, avec des tâches attribuées aux bonnes personnes.
C’est gérable tant que le volume reste faible. Mais face à des centaines de demandes, la gestion manuelle du staffing et la création de projets à partir de modèles ne sont plus viables.
Cet article présente une méthode simple pour automatiser ce processus à grande échelle en combinant deux fonctionnalités d’Asana :
- AI Studio, pour identifier les collaborateurs les plus pertinents en fonction de leurs compétences et de leur charge de travail.
- Script Actions, pour générer automatiquement un projet à partir d’un modèle et attribuer les rôles.
À noter : les Script Actions sont disponibles exclusivement avec les forfaits Enterprise.
Le résultat : une simple demande se transforme en un projet complet, structuré et doté de ressources, de manière totalement automatisée !
Partie 1 - Le cas d’usage
A - Ce qu’il est actuellement possible de faire manuellement
1 - Centraliser les demandes dans un projet et les analyser manuellement
2 - Identifier visuellement les ressources adéquates en fonction de leur charge de travail
3 - Créer un projet à partir d’un modèle et attribuer les rôles manuellement
3bis - (Alternative) Utiliser une règle pour convertir votre tâche en projet, mais perdre les avantages des rôles du modèle
Les rôles des modèles n’étant pas nativement compatibles avec les règles, les tâches devront être attribuées manuellement après la création du projet.
La problématique réside dans l’effort estimé : entre 5 et 20 minutes par projet, selon la complexité et la taille de l’équipe. Cela peut représenter 20 heures de travail, voire plus, pour 100 projets.
B. La solution de flux de travail automatisé 
📼 Cliquez ici si vous préférez découvrir ce cas d'usage en vidéo (disponible en anglais)
Veuillez m’excuser pour la qualité sonore, le mauvais microphone était sélectionné. ![]()
Le flux de travail automatisé de bout en bout est le suivant :
1 - Arrivée d’une demande dans un projet de réception
Une tâche est créée dans un projet type « Demandes » via un formulaire ou manuellement. Elle comporte tous les champs souhaités (type, priorité, etc.), mais doit au moins inclure une « échéance ». Le projet contient également des champs personnalisés de type « Personnes » (vides à ce stade) pour les rôles concernés (ex : Chef de projet, Spécialiste…).
2 - AI Studio assure le staffing de la tâche
La règle AI Studio :
-
Vérifie l’effectif de votre équipe (rôles, compétences)
-
Analyse la charge de travail et la capacité de votre équipe sur la période du projet
-
Sélectionne la personne la plus adaptée pour chaque rôle, remplit les champs « Personnes » et ajoute un commentaire
-
Déplace la tâche vers la section suivante, prête pour l’étape d’après
3 - Les Script Actions créent le projet et attribuent les tâches
Une Script Action se déclenche lorsque la tâche entre dans cette section et :
-
À partir d’un modèle de projet incluant des rôles
-
Crée un nouveau projet à partir d’un modèle.
-
Associe les rôles du modèle aux personnes renseignées dans les champs « Personnes » de la tâche.
-
Multihome la demande d’origine pour qu’elle soit facilement accessible depuis le nouveau projet.
-
Pour le demandeur, c’est magique : il soumet une demande structurée et, peu après, reçoit un commentaire contenant un lien vers son projet, déjà configuré et staffé.
Partie 2. Comment le mettre en œuvre (prompt et script inclus)
Avertissement : certaines parties de ce guide s’adressent à des utilisateurs avancés.
Cliquez ici pour consulter le guide étape par étape (incluant les prompts)
2.1. Prérequis
Vous aurez besoin de :
- Un accès à AI Studio
- (Optionnel) Un accès aux Script Actions
- Un projet de réception (ex: « Demandes ») avec :
- Un champ « Échéance »
- Un champ personnalisé de type « Personnes » par rôle (Chef de projet, Spécialiste, etc.)
- Des sections telles que « En attente d’attribution », « Staffing », « Créer projet avec rôles »…
- Un modèle de projet représentant le travail type pour une demande, comprenant :
- Des rôles de modèle définis qui correspondent à vos champs « Personnes »
- Des tâches de modèle attribuées à ces rôles
2.2. Étape par étape
Étape 1 – Configurer le flux de réception
- Créez ou réutilisez le projet de réception et son formulaire.
- Ajoutez les champs personnalisés « Personnes » pour chaque rôle à staffer.
- Configurez les sections que vous utiliserez pour suivre l’avancement.
Étape 2 – Créer l’agent de staffing AI Studio
- Déclenchez-le avec une règle lors de la création ou de la mise à jour d’une demande.
- Dans les instructions de l’agent, demandez-lui de :
- Filtrer l’équipe par rôle (et éventuellement par compétences).
- Utiliser une vue de charge de travail / capacité pour éviter de surcharger les collaborateurs.
- Choisir une personne par rôle et rédiger une brève explication en commentaire.
- Remplir les champs « Personnes » de la tâche.
- Déplacer la tâche vers :
- «
Créer projet avec rôles (via Script Actions) » quand tous les rôles sont pourvus. - « En attente d’attribution » si un élément est manquant ou ambigu.
📋 Cliquez ici pour voir le prompt complet
Vous êtes un assistant de staffing Asana pour les demandes informatiques (IT).
**Mission**
Remplissez les DEUX champs personnalisés de type « Personnes » sur la tâche actuelle :
- 🧑🍳 Chef de projet [IT]
- 🧑💻 Ingénieur technique [IT]
**Comment choisir les intervenants**
1) Ouvrez la page de l'équipe IT et identifiez les candidats via le champ « Rôle [IT] » :
- Pour le CP : choisissez parmi les personnes dont le Rôle [IT] = Project Manager.
- Pour le Tech : choisissez parmi les personnes dont le Rôle [IT] = Technical Engineer.
Page de l'équipe : IT (https://app.asana.com/0/1202805238464599/1209002045072288)
2) Ouvrez la page de charge de travail (Workload) IT et choisissez, parmi les candidats, la personne ayant la plus grande CAPACITÉ durant la période de la tâche (date de début et échéance).
Page de charge de travail : [IT] Team Workload (https://app.asana.com/0/1212848555821367/1212813398026822)
**Règles**
- Si la tâche n'a pas d'échéance → n'attribuez personne.
- Si vous ne trouvez pas de personne valide pour un rôle → laissez le champ vide.
- N'attribuez quelqu'un que si son Rôle [IT] correspond exactement au rôle requis.
- Évitez d'attribuer la même personne aux deux rôles, sauf si elle correspond clairement aux deux ET qu'elle dispose de la capacité nécessaire.
**Après la tentative de staffing (ajoutez TOUJOURS un commentaire)**
Publiez un commentaire bien mis en forme avec des emojis incluant :
- 📅 Échéance (ou « échéance manquante »)
- 🧑🍳 CP choisi (nom) + raison courte (« Rôle correspondant + capacité maximale »)
- 🧑💻 Tech choisi (nom) + raison courte (« Rôle correspondant + capacité maximale »)
- 🔎 Pages utilisées : Équipe + Charge de travail
- ✅ Résultat : « Staffing complet » OU « Attribution(s) manquante(s) : ... »
**Déplacez la tâche selon le résultat**
- Si les DEUX rôles sont remplis → déplacez la tâche vers la section :
« 🤖 Créer projet avec rôles (via Script Actions) »
- Si l'un ou les deux rôles ne sont pas remplis → déplacez la tâche vers la section :
« En attente d'attribution »
Étape 3 – Configurer la Script Action pour la création du projet
Déclenchez-la lorsqu’une tâche entre dans la section «
Créer projet avec rôles ». Dans le script, vous devrez :
- Lire la tâche et ses champs personnalisés de type « Personnes ».
- Appeler la fonction
instantiateProject(...)sur votre modèle, en transmettant : - Un nom de projet généré automatiquement.
- Un mapping
requested_rolesfaisant correspondre les rôles du modèle aux IDs utilisateurs récupérés dans les champs « Personnes ». - (Optionnel) Les
requested_datessi votre modèle utilise des variables de dates (par exemple : début = aujourd’hui, échéance = celle de la demande). - L’équipe de destination, si nécessaire dans votre espace de travail.
📋 Cliquez ici pour voir la Script Action complète
Avertissement : Ce script a été conçu pour notre propre environnement et nécessite une mise à jour des variables (GID) pour fonctionner dans le vôtre. Si vous n’êtes pas familier avec ces aspects techniques, la configuration peut s’avérer complexe. N’hésitez pas à solliciter un assistant IA pour vous guider dans l’adaptation de votre script. ![]()
// Code created by Asana Solutions Partner i.DO (ido-clarity.com).
async function run() {
log('=== SCRIPT START: Instantiate project from template + link back to request ===');
log(`workspace_gid: ${workspace_gid}`);
log(`project_gid: ${project_gid}`);
log(`task_gid: ${task_gid}`);
const projectTemplateGid = '1207291537152711'; // Software update template
// People custom fields on the triggering task
const PM_PEOPLE_CF_GID = '1212848577913693'; // 🧑🍳Project Manager [IT]
const TECH_PEOPLE_CF_GID = '1212848580115291'; // 🧑💻Technical Engineer [IT]
// Template role names (as they exist on the template)
const ROLE_PM_NAME = 'Project Manager';
const ROLE_TECH_NAME = 'Technical Engineer';
const TASK_PREFIX = '[Initial request - converted to project] ';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function getFirstPersonFromPeopleCustomField(task, customFieldGid) {
const cf = (task.custom_fields || []).find((x) => x.gid === customFieldGid);
if (!cf) return null;
const pv = cf.people_value;
if (!pv || !Array.isArray(pv) || pv.length === 0) return null;
return pv[0];
}
// --- Helpers for color & icon selection ---
function pickColorFromProjectName(name) {
const palette = [
'dark-blue',
'dark-green',
'dark-pink',
'dark-purple',
'dark-red',
'dark-teal',
'dark-orange',
'dark-brown'
];
if (!name || typeof name !== 'string') {
log('pickColorFromProjectName: no name provided, defaulting to dark-blue');
return 'dark-blue';
}
const trimmed = name.trim();
if (!trimmed) {
log('pickColorFromProjectName: empty name after trim, defaulting to dark-blue');
return 'dark-blue';
}
const firstLetter = trimmed[0].toUpperCase();
const code = firstLetter.charCodeAt(0);
if (code < 65 || code > 90) {
log(`pickColorFromProjectName: first char "${firstLetter}" is not A-Z, defaulting to dark-blue`);
return 'dark-blue';
}
const index = (code - 65) % palette.length;
const color = palette[index];
log(`pickColorFromProjectName: name="${name}", firstLetter="${firstLetter}", color="${color}"`);
return color;
}
function pickIconFromProjectName(name) {
const lower = (name || '').toLowerCase();
const mappings = [
{ regex: /(security|secur|auth|password|2fa|mfa)/, icon: 'shield' },
{ regex: /(bug|incident|issue|defect|error)/, icon: 'bug' },
{ regex: /(infra|server|hosting|cloud|aws|gcp|azure)/, icon: 'cloud' },
{ regex: /(deploy|release|launch|rollout|go live|golive)/, icon: 'rocket' },
{ regex: /(data|report|kpi|metric|analytics|dashboard)/, icon: 'graph' }
];
for (const m of mappings) {
if (m.regex.test(lower)) {
log(`pickIconFromProjectName: matched "${m.regex}" for name="${name}", icon="${m.icon}"`);
return m.icon;
}
}
log(`pickIconFromProjectName: no thematic match for name="${name}", defaulting to "star"`);
return 'star';
}
// --- Helpers for errors: move task + comment ---
async function moveTaskToPendingAssignmentSection(taskGid, projectGid) {
if (typeof sectionsApiInstance === 'undefined') {
log('sectionsApiInstance is not available; cannot move task to "Pending Assignment".');
return;
}
log(`Looking for section "Pending Assignment" in project ${projectGid}...`);
const rawSections = await sectionsApiInstance.getSectionsForProject(projectGid, {
opt_fields: 'gid,name'
});
const sections = rawSections && rawSections.data ? rawSections.data : rawSections;
if (!Array.isArray(sections)) {
log('getSectionsForProject returned an unexpected payload; cannot move task.');
return;
}
const targetSection = sections.find(
(s) => (s.name || '').trim().toLowerCase() === 'pending assignment'
);
if (!targetSection) {
log('Section "Pending Assignment" was not found; task will not be moved.');
return;
}
log(`Moving task ${taskGid} to section "${targetSection.name}" (${targetSection.gid})...`);
// Script Actions env: section_gid first, then opts.body.data
const addOpts = {
body: {
data: { task: taskGid }
}
};
await sectionsApiInstance.addTaskForSection(targetSection.gid, addOpts);
log('Task moved back to "Pending Assignment" section.');
}
async function addErrorCommentToTask(taskGid, message) {
if (typeof storiesApiInstance === 'undefined') {
log('storiesApiInstance is not available; cannot add an error comment.');
return;
}
const text =
'❌ Project not created by script\n\n' +
message;
log('Adding error comment to the request task: ' + text.replace(/\n/g, ' '));
await storiesApiInstance.createStoryForTask(
{ data: { text } },
taskGid
);
log('Error comment added successfully.');
}
// 1) Load triggering task with needed fields
log('Fetching triggering task with People custom fields + due dates...');
const rawTaskResp = await tasksApiInstance.getTask(task_gid, {
opt_fields:
'name,due_on,due_at,custom_fields.gid,custom_fields.name,custom_fields.resource_subtype,custom_fields.people_value.gid,custom_fields.people_value.name'
});
const task = rawTaskResp && rawTaskResp.data ? rawTaskResp.data : rawTaskResp;
log(`Triggering task loaded: "${task.name}" (${task_gid})`);
const pmPerson = getFirstPersonFromPeopleCustomField(task, PM_PEOPLE_CF_GID);
const techPerson = getFirstPersonFromPeopleCustomField(task, TECH_PEOPLE_CF_GID);
log(`🧑🍳 PM field -> ${pmPerson ? `${pmPerson.name} (${pmPerson.gid})` : 'NOT SET'}`);
log(`🧑💻 TECH field -> ${techPerson ? `${techPerson.name} (${techPerson.gid})` : 'NOT SET'}`);
if (!pmPerson || !techPerson) {
log('STOP: Both roles must be filled on the task before creating the project from template.');
const errorMessage =
'Both roles must be filled on this request before a project can be created from the template. ' +
'Please set both 🧑🍳 Project Manager [IT] and 🧑💻 Technical Engineer [IT], then re-run the rule. ' +
'The task was moved back to the "Pending Assignment" section.';
await moveTaskToPendingAssignmentSection(task_gid, project_gid);
await addErrorCommentToTask(task_gid, errorMessage);
log('=== SCRIPT END (no instantiation - missing roles) ===');
return;
}
// 2) Fetch workspace to know if it is an organization (team vs workspace payload differences)
log('Fetching workspace to check is_organization...');
const rawWsResp = await workspacesApiInstance.getWorkspace(workspace_gid, {
opt_fields: 'name,is_organization'
});
const ws = rawWsResp && rawWsResp.data ? rawWsResp.data : rawWsResp;
log(`Workspace: "${ws.name}", is_organization: ${ws.is_organization}`);
// 3) Fetch project template details (roles + date variables + team)
log('Fetching project template (requested_roles + requested_dates + team)...');
const rawTmplResp = await projectTemplatesApiInstance.getProjectTemplate(projectTemplateGid, {
opt_fields:
'name,team.gid,team.name,requested_roles.gid,requested_roles.name,requested_dates.gid,requested_dates.name,requested_dates.description'
});
const template = rawTmplResp && rawTmplResp.data ? rawTmplResp.data : rawTmplResp;
log(`Template: "${template.name}" (${projectTemplateGid})`);
if (template.team) {
log(`Template team: ${template.team.name} (${template.team.gid})`);
} else {
log('Template team: none');
}
const requestedRolesFromTemplate = template.requested_roles || [];
log(`Template requested_roles count: ${requestedRolesFromTemplate.length}`);
for (const r of requestedRolesFromTemplate) {
log(`ROLE -> "${r.name}" (${r.gid})`);
}
const roleGidByName = {};
for (const r of requestedRolesFromTemplate) roleGidByName[r.name] = r.gid;
const pmRoleGid = roleGidByName[ROLE_PM_NAME];
const techRoleGid = roleGidByName[ROLE_TECH_NAME];
if (!pmRoleGid || !techRoleGid) {
log('STOP: Could not find role GIDs on the template by expected role names.');
log(`Expected role "${ROLE_PM_NAME}" -> ${pmRoleGid || 'NOT FOUND'}`);
log(`Expected role "${ROLE_TECH_NAME}" -> ${techRoleGid || 'NOT FOUND'}`);
const errorMessage =
'The script could not find one or both expected roles ("Project Manager" / "Technical Engineer") on the project template. ' +
'Please check that both roles exist on the template and are correctly named, then re-run the rule.';
await addErrorCommentToTask(task_gid, errorMessage);
log('=== SCRIPT END (no instantiation - template roles missing) ===');
return;
}
// 4) Build requested_dates (if template uses date variables)
const todayStr = new Date().toISOString().slice(0, 10);
const taskDueStr = task.due_on
? task.due_on
: task.due_at
? String(task.due_at).slice(0, 10)
: null;
log(`Today (UTC date): ${todayStr}`);
log(`Task due date (best effort): ${taskDueStr || 'none'}`);
const requestedDatesFromTemplate = template.requested_dates || [];
log(`Template requested_dates count: ${requestedDatesFromTemplate.length}`);
const requested_dates = [];
for (const d of requestedDatesFromTemplate) {
const name = (d.name || '').toLowerCase();
// Lightweight heuristic:
// - "start/kickoff" -> today
// - "due/end/launch" -> task due date if available (else today)
let value = todayStr;
if (taskDueStr && /(due|end|finish|deadline|launch|go live|golive)/.test(name)) {
value = taskDueStr;
}
requested_dates.push({ gid: d.gid, value });
log(`DATE VAR -> "${d.name}" (${d.gid}) => value: ${value}`);
}
// 5) Build requested_roles
const requested_roles = [
{ gid: pmRoleGid, value: pmPerson.gid },
{ gid: techRoleGid, value: techPerson.gid }
];
log(`Mapping role "${ROLE_PM_NAME}" (${pmRoleGid}) -> ${pmPerson.name} (${pmPerson.gid})`);
log(`Mapping role "${ROLE_TECH_NAME}" (${techRoleGid}) -> ${techPerson.name} (${techPerson.gid})`);
// 6) Build instantiation payload
const projectName = `${template.name} - ${task.name}`;
const instantiateData = {
name: projectName,
requested_roles
};
if (requested_dates.length > 0) {
instantiateData.requested_dates = requested_dates;
}
if (ws.is_organization) {
if (template.team?.gid) {
instantiateData.team = template.team.gid;
log(`Including team in payload (org workspace): ${template.team.name} (${template.team.gid})`);
} else {
log('WARNING: Workspace is org but template.team is missing. Not setting team in payload.');
}
} else {
instantiateData.workspace = workspace_gid;
log(`Including workspace in payload (non-org workspace): ${workspace_gid}`);
}
const instantiateOpts = { body: { data: instantiateData } };
log(`Instantiating project with name: ${projectName}`);
log('instantiateProject body: ' + JSON.stringify(instantiateOpts.body));
// 7) Instantiate project (returns a Job)
const rawJobResponse = await projectTemplatesApiInstance.instantiateProject(projectTemplateGid, instantiateOpts);
const jobResponse = rawJobResponse && rawJobResponse.data ? rawJobResponse.data : rawJobResponse;
log('InstantiateProject job response: ' + JSON.stringify(jobResponse));
const job = jobResponse || null;
if (!job?.gid) {
log('No job gid returned. Cannot poll job.');
const errorMessage =
'The project template instantiation did not return a job identifier, so the script could not complete project creation. ' +
'Please try running the rule again; if the problem persists, check the template configuration.';
await addErrorCommentToTask(task_gid, errorMessage);
log('=== SCRIPT END (no instantiation - missing job gid) ===');
return;
}
log(`Job gid: ${job.gid}, status: ${job.status || 'unknown'}`);
// 8) Poll job to get the created project
let newProjectGid = null;
let newProjectName = null;
let newProjectUrl = null;
if (typeof jobsApiInstance === 'undefined') {
log('jobsApiInstance is not available; cannot poll for new_project.');
} else {
for (let i = 1; i <= 12; i++) {
const rawJobCheck = await jobsApiInstance.getJob(job.gid, {
opt_fields: 'status,new_project.gid,new_project.name,new_project.permalink_url'
});
const j = rawJobCheck && rawJobCheck.data ? rawJobCheck.data : rawJobCheck;
log(`Poll #${i}: status=${j.status}`);
if (j.new_project?.gid) {
newProjectGid = j.new_project.gid;
newProjectName = j.new_project.name;
newProjectUrl = j.new_project.permalink_url || null;
log(`✅ New project created: ${newProjectName} (${newProjectGid})`);
log(`Project URL (from job): ${newProjectUrl || '(none returned)'}`);
break;
}
await sleep(1000);
}
}
// Best effort: if URL missing but we have project gid, fetch permalink_url
if (newProjectGid && !newProjectUrl && typeof projectsApiInstance !== 'undefined') {
log('Fetching new project to get permalink_url...');
const rawProjResp = await projectsApiInstance.getProject(newProjectGid, {
opt_fields: 'name,permalink_url,color,icon'
});
const projResp = rawProjResp && rawProjResp.data ? rawProjResp.data : rawProjResp;
newProjectName = projResp?.name || newProjectName;
newProjectUrl = projResp?.permalink_url || null;
log(`Project URL (from project): ${newProjectUrl || '(none returned)'}`);
}
// If we still don't have the created project, stop here
if (!newProjectGid) {
log('Could not retrieve new_project from job polling. Skipping prefix/comment/multi-home.');
const errorMessage =
'The script could not retrieve the new project after instantiation. ' +
'The project may not have been created correctly. Please check the IT software update projects and try again.';
await addErrorCommentToTask(task_gid, errorMessage);
log('=== SCRIPT END (no instantiation - project not found from job) ===');
return;
}
// 9) Set project color & icon based on project name
if (typeof projectsApiInstance === 'undefined') {
log('projectsApiInstance is not available; cannot update project color or icon.');
} else {
const finalProjectName = newProjectName || projectName;
const color = pickColorFromProjectName(finalProjectName);
const icon = pickIconFromProjectName(finalProjectName);
const updateBody = {
data: {
color,
icon
}
};
log(`Updating new project ${newProjectGid} with color="${color}" and icon="${icon}"`);
// IMPORTANT: body first, then project_gid (aligned with Script Actions Gallery example)
const rawUpdatedProject = await projectsApiInstance.updateProject(updateBody, newProjectGid);
const safeData = rawUpdatedProject && rawUpdatedProject.data ? rawUpdatedProject.data : rawUpdatedProject;
log('Project color/icon updated summary: ' + JSON.stringify({
gid: safeData?.gid || newProjectGid,
color: safeData?.color,
icon: safeData?.icon
}));
}
// 10) Prefix the request task name
const currentName = task.name || '';
const desiredName = currentName.startsWith(TASK_PREFIX) ? currentName : (TASK_PREFIX + currentName);
if (desiredName !== currentName) {
log('Updating request task name with prefix...');
const updateTaskBody = { data: { name: desiredName } };
await tasksApiInstance.updateTask(updateTaskBody, task_gid, {});
log('Task name updated: ' + desiredName);
} else {
log('Task name already has prefix; skipping rename.');
}
// 11) Multi-home the request task into the created project
log(`Multi-homing request task into new project (${newProjectGid})...`);
if (typeof tasksApiInstance === 'undefined') {
log('tasksApiInstance is not available; cannot multi-home the task into the new project.');
} else {
// Body-first convention, aligned avec d'autres scripts qui fonctionnent ici
const addProjectBody = { data: { project: newProjectGid } };
await tasksApiInstance.addProjectForTask(addProjectBody, task_gid, {});
log('Task successfully added to the new project.');
}
// 12) Add a comment on the request task with project name + link
if (typeof storiesApiInstance === 'undefined') {
log('storiesApiInstance is not available; cannot add a success comment.');
log('=== SCRIPT END (success but no comment) ===');
return;
}
const linkLine = newProjectUrl ? `🔗 Project link: ${newProjectUrl}` : '🔗 Project link: (not available)';
const commentText =
`✅ Project created from this request\n\n` +
`📌 Project: ${newProjectName || '(unknown)'}\n` +
`${linkLine}\n\n` +
`👥 Roles\n` +
`🧑🍳 PM: ${pmPerson.name}\n` +
`🧑💻 Tech: ${techPerson.name}\n\n` +
`📎 This request was added to the project (multi-homed).`;
log('Adding comment to the request task:\n' + commentText);
await storiesApiInstance.createStoryForTask(
{ data: { text: commentText } },
task_gid
);
log('Comment added successfully.');
log('=== SCRIPT END (success) ===');
}
run();
Une fois cette configuration opérationnelle pour une équipe, l’adapter n’est plus qu’une question de remplacement du projet, des rôles et du modèle.
Arthur | Asana Expert ![]()
J’aide les équipes à tirer le meilleur parti d’Asana + de l’IA - plus de conseils














