With standard Roll20 syntax... potentially with a trick from RainbowEncoder. However, if scripts are available, you can do this with the MetaScriptToolbox. Here is a proof of concept with the rolls broken out so you can see what is individually happening in each step: !&{template:default} {{name=Proof of Concept}} {{Roll 0=[[6d6]]}}{{Roll 1=[\][\] {&math min(3,{&eval}getDiceByVal($[[0]] 1 all count){&/eval})}d6 \]\]}} {{Roll 2= [\\][\\] {{&eval}getDiceByVal($[[0]] 1-6 all list|,){&/eval}{&if {&eval}getDiceByVal($[[0]] 1 all count){&/eval} > 0},{\&eval}getDiceByVal($[[1]] 1-6 all list|,){\&/eval}({&end})}kh3 \\]\\] }}{&simple} And here is the roll presented as just itself, without a template: ![\\][\\] {{&eval}getDiceByVal([[6d6]] 1-6 all list|,){&/eval}{\&if {&eval}getDiceByVal($[[0]] 1 all count){&/eval} > 0},{\&eval}getDiceByVal([\][\] {&math min(3,{&eval}getDiceByVal($[[0]] 1 all count){&/eval})}d6 \]\] 1-6 all list|,){\&/eval}({\&end})}kh3 \\]\\] {&simple} What is Happening Roll 0 has just your original number of d6, without worrying about keeping the highest 3: [[6d6]] Roll 1 is a deferred roll that uses the result of Roll 0. That is fed to a Plugger EVAL statement designed to count the dice that show "1": {&eval}getDiceByVal($[[0]] 1 all count){&/eval} We want to keep as many as 3 of these dice, so we feed the result of that into a MathOps tag giving us the minimum value between 3 and the result we just produced: {&math min(3, ...previous eval stmt... )} And finally, the result of the MathOps equation is used as a new number of d6 in a deferred roll: [\][\] {&math min(...)}d6 \]\] Roll 2 is where we will finally keep the highest 3 of all of our rolls. To build our list, we'll get all the dice from Roll 0 (our original roll) plus all of the dice from our Roll 1 (where the 1s were re-rolled). We retrieve them as a list this time, and use comma delimiters. Here from Roll 0: {&eval}getDiceByVal([[6d6]] 1-6 all list|,){&/eval} ...and here from Roll 1: {\&eval}getDiceByVal($[[1]] 1-6 all list|,){\&/eval} Note that the EVAL block is deferred 1 time. This allows for the deferred roll it refers to (Roll 1) to resolve. That EVAL block getting the dice from Roll 1 is fed to an APILogic IF block to only include it (and the comma that would precede it) if there were 1s detected in the original roll (so we know that there will be dice in Roll 1). We use the same MathOps construction to get the: {&if {&eval}getDiceByVal($[[0]] 1 all count){&/eval} > 0},{\&eval}getDiceByVal($[[1]] 1-6 all list|,){\&/eval}({&end}) Note that the closing END tag is enclosed in parentheses. That's because we're embedding this in the brackets of a keep-high operation: { ... }kh3 ...and without the parentheses, the closing brace of the END tag might combine with the closing brace of the keep-high construction, resulting in a double brace that would look like the end of a template part. Roll 2 is therefore built to be the keep-high-3 operation from all of the dice we might have rolled. Incorporate Queries If necessary, the number of 1s potentially rerolled could be handled with a query (changing the maximum number of 1s that could be rerolled). Also, if necessary, the number of dice to keep from the set of dice we've rolled by the end can also be handled with a query. In fact, if the rule is that these would be the same value (you can reroll X amount of 1s, and you will keep X amount from the result), then those two values could be driven off the same query.