Hi all ![]()
I’m Arthur a Forum leader, and passionate about AI and automations in Asana.
Please me know what you think about this use-case, and happy to answer questions. ![]()
Where Asana meets automation and scalability
Many teams live in the same loop: lots of similar requests come in (software updates, campaigns, onboardings, process changes…), and each one really deserves a proper project with tasks assigned to the right people.
That’s still manageable when you only have a few projects. But when you’re dealing with hundreds of similar requests, manually staffing each one and creating projects from templates doesn’t scale anymore.
This article shows a simple way to scale it with full automation by combining two Asana features:
- AI Studio, to choose the right people based on skills and workload
- Script Actions, to create a project from a template and assign those roles automatically
Please note that Script Actions are available exclusively on Enterprise plans
The result: one request converted to a fully staffed, fully structured project, fully automated!
Part 1 - The use-case
A- What is currently possible, when done by hand
1- Store requests in a project and analyze them manually
2- Visually identify the right ressources based on their workload
3- Create a project from a template and assign the roles manually
3bis- (Alternative) Use a rule to convert your task to a project, but loose the benefits of the template’s role
As template’s roles are not natively compatible with rules, tasks will need to be assigned manually after project creation
The pain is the estimated effort: 5 to 20 minutes by project, based on complexity and team size. That’s could represent 20 hours for 100 projects, or more.
B. The automated workflow solution 
📼 Click if you prefer to watch the use-case in video
Sorry for sound quality, the wrong microphone was selected
End-to-end automated workflow is:
1- A request arrives in an intake project
A task is created in a project like “Requests” via a form or manually. It has any desired fields (type, priority…) but at least “due date”, and the project contains empty People custom fields for the roles you care about (e.g. Project Manager, Specialist…).
2- AI Studio staffs the task
The AI Studio rule:
-
Checks your team roster (roles, skills)
-
Checks your team workload & capacity during the project timeframe
-
Chooses the best person for each role, fills the People fields & adds a comment
-
Moves the task to next section, ready for next step
3- Script Actions creates the project and assign tasks
A Script Action triggers when the task enters that section, and:
-
From a project template with roles
-
Creates a new project from a template
-
Maps the template roles to the people in the task’s People fields
-
Multihome the original request so it’s easy to find from the new project
-
For the requester, it feels like magic: they submit a structured request and, shortly after, see a comment with a link to a staffed project created for them.
Part 2. How to implement it (prompt & script included)
Disclaimer: Some parts of that guide are for advanced users.
Click here to see the step-by-step guide (including prompts)
2.1. Prerequisites
You’ll need:
- Access to AI Studio
- (Optional) Access to Script Actions
- An intake project (e.g. “Requests”) with:
-
- A due date field
- One People custom field per role (Project Lead, Specialist, etc.)
- Sections such as “Pending assignment”, “
Staffing”, “
Create project with roles”..
- A project template that represents the typical work for one request, with:
- Defined ** template’s roles** that match your People fields
- Tasks of the template assigned to the roles
2.2. Step by step
Step 1 – Configure the intake workflow
- Create or reuse the intake project and its form.
- Add the People custom fields for each role you want to staff.
- Set up the sections you’ll use to track progress
Step 2 – Build the AI Studio staffing agent
- Trigger it with a rule when a new request is created or updated.
- In the agent instructions, tell it to:
- Filter the team by role (and optionally skills).
- Use a workload / capacity view to avoid overloading people.
- Pick 1 person per role and write a short explanation in a comment.
- Fill the People fields on the task.
- Move the task to:
- **“
Create project with roles (by Script Actions)”***when all roles are staffed, or - “Pending assignment” if something is missing or ambiguous.
📋 Click here to see the full prompt
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 – Build the Script Action that creates the project
- Trigger it when a task enters the “Create project with roles” section.
- In the script, you:
- Read the task and its People custom fields.
- Call
instantiateProject(...)on your template, passing: - A generated project name
- A
requested_rolesmapping from template roles → user IDs from the People fields - (Optional)
requested_datesif your template expects date variables (for example, start = today, due = the request’s due date) - The team, if needed in your workspace
📋 Click here to see the full Script Action
Disclaimer: This script was made for our environement, and requires some update on the variable (GID) to be functional in yours; and if you don’t understand this technical part, you might struggle. But feel free to use an AI Assistant to guide you adapting your 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();
Once this is working for one team, adapting it is mostly a matter of swapping the project, roles and template.
Arthur | Asana Expert ![]()
I help teams get more from Asana + AI - more tips















