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

  1. Crea o riutilizza il progetto di intake e il relativo modulo.
  2. Aggiungi i campi personalizzati di tipo Persone per ogni ruolo da assegnare.
  3. Configura le sezioni che utilizzerai per seguire l’avanzamento.

 

Step 2 – Creare l’agente di assegnazione in AI Studio

  1. Attivalo tramite una regola quando una nuova richiesta viene creata o aggiornata.
  2. 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à.)

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

  1. Attivala quando un’attività entra nella sezione «Creazione progetti con ruolo».
  2. 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

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.