Thank you! On second thought I don't want to use charIds as an array since I am running more than one campaign and your original script works just fine once I put it in a button. I want to summon the whole party from a "party token" and ping the map to that area. It's working great now! I just modified your script to draw above the selected token. The party token calls the script from a token action. on('ready',()=>{ // function to grab the page a player is on, even if split from the party or a gm const getPageForPlayer = (playerid) => { let player = getObj('player',playerid); if(playerIsGM(playerid)){ return player.get('lastpage'); } let psp = Campaign().get('playerspecificpages'); if(psp[playerid]){ return psp[playerid]; } return Campaign().get('playerpageid'); }; // function to convert from any user library url to a valid thumb url for the api const getCleanImgsrc = (imgsrc) => { let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); if(parts) { return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); } return; }; // some text matching functions for easily taking name parts and finding matches const keyFormat = (text) => (text && text.toLowerCase().replace(/\s+/g,'')) || undefined; const matchKey = (keys,subject) => subject && !_.isUndefined(_.find(keys,(o)=>(-1 !== subject.indexOf(o)))); // handle the !ct command // !ct --some name part --some other name part on('chat:message', (msg) => { if('api'===msg.type && /^!ct\s+/.test(msg.content)){ let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); let pageid = getPageForPlayer(msg.playerid); let page = getObj('page',pageid); let selected = msg.selected; if (selected===undefined){ sendChat("API","No token selected."); return; } // get each of the name parts let args = msg.content.split(/\s+--/).slice(1); // build them as matching fragments let keys = args.map(keyFormat); // find all the characters that match those fragments let characters = findObjs({type:'character'}) .filter(c=>matchKey(keys,keyFormat(c.get('name')))); // store some information about the page and where we've made tokens (see below) let tok = getObj("graphic",selected[0]._id); let lastX = tok.get("left")-140; let lastY = tok.get("top")-70; const pageX = page.get('width')*70; const pageY = page.get('height')*70; // getting X marches the creation point across the top of the screen, // then back to the left like a typewriter const nextX = ()=>{ lastX+=70; if(pageX<lastX){ lastX=35; lastY+=70; if(pageY<lastY){ lastY=35; } } return lastX; }; // just gets the current Y to use const nextY = ()=> lastY; // asynchronous function to create tokens const burndown = ()=>{ // while there are still characters, grab the next one if(characters.length){ let c = characters.shift(); // get the defaulttoken data, which is an asynchronous call c.get('defaulttoken',(dt) => { // decode it from JSON let props = JSON.parse(dt); // if it decoded, we'll process it if(props){ // get a cleaned up imgsrc props.imgsrc = getCleanImgsrc(props.imgsrc); // for good images, we'll make tokens if(props.imgsrc) { // add the page id, top and left properties props.pageid = pageid; props.top = nextY(); props.left = nextX(); // create the token createObj('graphic',props); } else { // for Marketplace images, report to the caller sendChat('', `/w "${who}" <div>Cannot create <code>${c.get('name')}</code> because it has a Marketplace image.</div>`); } } // defer call to the next character to process setTimeout(burndown,0); }); } }; // start working through the queue burndown(); } }); });