Here's a script that will do what you need. The styling is basic and could be improved with extra work. The output looks like this: The bottom one is an example of a situation where the player needed to roll multiple times. There's an option in the script to disable displaying previous rolls, but I thought I'd include this to see if you liked it. It doesn't take up too much clutter. Here's a campaign where you can test it out: <a href="https://app.roll20.net/join/4097815/MWGvsA" rel="nofollow">https://app.roll20.net/join/4097815/MWGvsA</a> If you type this in chat you'll get a button which anyone can click to use the script: [Ability Rolls](!dndabilityrolls) I've added this as a universal Macro in the above campaign, named ability-rolls. To use the script in your own campaigns, your GM will need to copy and paste this script into your campaigns scripts tab. on('ready', function () { const showAllRolls = 1, // change this to 0 if you dont want to display previous rolls. getSpeaker = function (msg) { let characters = findObjs({ _type: 'character' }); let speaking; characters.forEach(function (chr) { if (chr.get('name') == msg.who) speaking = chr; }); let speaker = speaking ? 'character|' + speaking.id : 'player|' + msg.playerid; return speaker; }, getTitle = function (speaker) { const args = speaker.split('|'); const type = args[0]; const id = args[1]; let title = ''; if (type === 'player') { title = getObj("player", id).get("displayname") || ''; } else if (type === 'character') { title = getAttrByName(id, 'character_name') || ''; } return title; }, processInlinerolls = function (msg) { if (_.has(msg, 'inlinerolls')) { return _.chain(msg.inlinerolls) .reduce(function (previous, current, index) { previous['$[[' + index + ']]'] = current.results.total || 0; return previous; }, {}) .reduce(function (previous, current, index) { return previous.replace(index, current); }, msg.content) .value(); } else { return msg.content; } }; on('chat:message', function (msg) { if (msg.type === "api" && msg.content.match("!dndabilityrolls") && !_.has(msg, 'inlinerolls')) { let speaker = getSpeaker(msg); sendChat(speaker, `!dndabilityrolls--${speaker}--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]`); } else if (msg.type === "api" && msg.content.match("!dndabilityrolls") && _.has(msg, 'inlinerolls')) { const message = processInlinerolls(msg); const args = message.split('--'); const speaker = args[1]; const title = getTitle(speaker); let attempts = ''; if (showAllRolls) { for (let i = 8; i < args.length; i++) { attempts += args[i]; } } let output = `&{template:default} {{name=Ability Scores: ${title}}} ${attempts} {{`; let total = 0; for (let i = 2; i < 8; i++) { output += `${args[i]} `; if (i !== 7) output += '- '; total += parseInt(args[i], 10) || 0; } output += `=Total: ${total}}}`; if (total < 62 || total > 82) { attempts += `--{{${args[2]} - ${args[3]} - ${args[4]} - ${args[5]} - ${args[6]} - ${args[7]}=Total: ${total}}}`; sendChat(speaker, `!dndabilityrolls--${speaker}--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--[[4d6kh3]]--${attempts}`); } else { sendChat(speaker, output); } } }); });