Completare l'automazione combinando due funzionalità di Asana:
Asana Studio e Script Actions
(prompt e script inclusi)
Questo tutorial ti guida nella configurazione di un flusso di lavoro completo che combina AI Studio e Script Actions per automatizzare lo staffing e la creazione di progetti in Asana. Accompagna il nostro articolo del blog sull’argomento, dove troverai il contesto completo e i casi d’uso correlati
⚠️ Disclaimer:Alcune parti di questa guida sono pensate per utenti esperti.
Come implementarlo (prompt e script inclusi)
Prerequisiti
Avrai bisogno di:
Accesso ad AI Studio
(Opzionale) Accesso alle Script Actions
Un progetto di intake (es.: «Richieste») con:
Un campo data di scadenza
Un campo personalizzato di tipo Persone per ruolo (Project Manager, Specialista, ecc.)
Sezioni come «Assegnazione in attesa», «Assegnazione», «Creazione progetti con ruolo»…
Un modello di progetto che rappresenta il lavoro tipo per una richiesta, con:
Ruoli del modello definiti corrispondenti ai tuoi campi Persone
Attività del modello assegnate ai ruoli
Configurazione
Step 1 – Configurare il flusso di lavoro di intake
Crea o riutilizza il progetto di intake e il relativo modulo.
Aggiungi i campi personalizzati di tipo Persone per ogni ruolo da assegnare.
Configura le sezioni che utilizzerai per seguire l’avanzamento.
Step 2 – Creare l’agente di assegnazione in AI Studio
Attivalo tramite una regola quando una nuova richiesta viene creata o aggiornata.
Nelle istruzioni dell’agente, chiedigli di:
Filtrare il team per ruolo (ed eventualmente per competenze).
Utilizzare una vista carico di lavoro / capacità per evitare di sovraccaricare le persone.
Scegliere 1 persona per ruolo e scrivere una breve motivazione della scelta tramite un commento.
Compilare i campi Persone sull’attività.
Spostare l’attività verso:
«Creazione progetti con ruolo» quando tutti i ruoli sono assegnati, oppure
«Assegnazione in attesa» se qualcosa manca o è ambiguo.
(Nel nostro esempio, il prompt di AI Studio e il codice della Script Action sono in inglese – vedi la descrizione dell’attività.)
Clicca qui per vedere il prompt completo
You are an Asana staffing assistant for IT requests.
**Task**
Fill BOTH People custom fields on the current task:
- 🧑🍳Project Manager [IT]
- 🧑💻Technical Engineer [IT]
**How to pick people**
1) Open the IT Team page and find candidates using the field “Role [IT]”:
- For PM: pick from people whose Role [IT] = Project Manager
- For Tech: pick from people whose Role [IT] = Technical Engineer
Team page: [IT](https://app.asana.com/0/1202805238464599/1209002045072288)
2) Open the IT Workload page and pick, among the candidates, the person with the MOST capacity during the task timeframe (start date and end date).
Workload page: [[IT] Team Workload](https://app.asana.com/0/1212848555821367/1212813398026822)
**Rules**
- If the task has no due date → do NOT assign anyone.
- If you can’t find a valid person for one role → leave it empty.
- Only assign someone if their Role [IT] matches the role.
- Prefer not assigning the same person to both roles unless they clearly match both roles AND have capacity.
**After you attempt staffing (ALWAYS add a comment)**
Post a nicely formatted comment with emojis that includes:
- 📅 Due date (or “missing due date”)
- 🧑🍳 PM chosen (name) + short reason (“Role match + highest capacity”)
- 🧑💻 Tech chosen (name) + short reason (“Role match + highest capacity”)
- 🔎 Pages used: Team + Workload
- ✅ Result: “Staffing complete” OR “Missing assignment(s): …”
**Move the task based on outcome**
- If BOTH roles are filled → move task to section:
“🤖 Create project with roles (by Script Actions)”
- If ONE or BOTH roles are not filled → move task back to section:
“Pending Assignment”
Step 3 – Creare la Script Action che genera il progetto
Attivala quando un’attività entra nella sezione «Creazione progetti con ruolo».
Nello script:
Leggi l’attività e i suoi campi personalizzati di tipo Persone.
Chiama instantiateProject(...) sul tuo modello, passando:
Un nome di progetto generato
Un mapping requested_roles dai ruoli del modello agli identificativi utente ricavati dai campi Persone
(Opzionale) requested_dates se il tuo modello utilizza variabili di data (ad esempio, inizio = oggi, scadenza = data di scadenza della richiesta)
Il team, se necessario nel tuo spazio di lavoro
Clicca qui per vedere la Script Action completa
Avvertenza: Questo script è stato progettato per il nostro ambiente e richiede l’aggiornamento di alcune variabili (GID) per funzionare nel tuo. Se non hai dimestichezza con questa parte tecnica, potrebbe risultare difficile. Non esitare a utilizzare un assistente AI per aiutarti ad adattare il tuo 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();
Una volta che funziona per un team, adattarlo consiste principalmente nel sostituire il progetto, i ruoli e il modello.