diff --git a/juno_scheduler/src/compile.rs b/juno_scheduler/src/compile.rs
index 3c288ca7a61c855b3982ba6ed66c215c9e2942fe..7b8c5020de1688bc509a07ed16226c1d3714ecd4 100644
--- a/juno_scheduler/src/compile.rs
+++ b/juno_scheduler/src/compile.rs
@@ -488,6 +488,29 @@ fn compile_expr(
                 rhs: Box::new(rhs),
             }))
         }
+        parser::Expr::Tuple {
+            span: _,
+            exps,
+        } => {
+            let exprs = exps.into_iter()
+                .map(|e| compile_exp_as_expr(e, lexer, macrostab, macros))
+                .fold(Ok(vec![]),
+                    |mut res, exp| {
+                        let mut res = res?;
+                        res.push(exp?);
+                        Ok(res)
+                })?;
+            Ok(ExprResult::Expr(ir::ScheduleExp::Tuple { exprs }))
+        }
+        parser::Expr::TupleField {
+            span: _,
+            lhs,
+            field,
+        } => {
+            let lhs = compile_exp_as_expr(*lhs, lexer, macrostab, macros)?;
+            let field = lexer.span_str(field).parse().expect("Parsing");
+            Ok(ExprResult::Expr(ir::ScheduleExp::TupleField { lhs: Box::new(lhs), field }))
+        }
     }
 }
 
diff --git a/juno_scheduler/src/ir.rs b/juno_scheduler/src/ir.rs
index 3a087c0d40093c6363e67332c1bd489f22727a42..71a185ba65b4cd5ade237cbfe164cce8067a475c 100644
--- a/juno_scheduler/src/ir.rs
+++ b/juno_scheduler/src/ir.rs
@@ -127,6 +127,13 @@ pub enum ScheduleExp {
         lhs: Box<ScheduleExp>,
         rhs: Box<ScheduleExp>,
     },
+    Tuple {
+        exprs: Vec<ScheduleExp>,
+    },
+    TupleField {
+        lhs: Box<ScheduleExp>,
+        field: usize,
+    },
     // This is used to "box" a selection by evaluating it at one point and then
     // allowing it to be used as a selector later on
     Selection {
diff --git a/juno_scheduler/src/lang.y b/juno_scheduler/src/lang.y
index 3b030e1d42bdb970cdfa67d21c4198dc89edea9e..5903e42941cef535128765cb6ab98601987d6d84 100644
--- a/juno_scheduler/src/lang.y
+++ b/juno_scheduler/src/lang.y
@@ -56,10 +56,14 @@ Expr -> Expr
       { Expr::String { span: $span } }
   | Expr '.' 'ID'
       { Expr::Field { span: $span, lhs: Box::new($1), field: span_of_tok($3) } }
+  | Expr '.' 'INT'
+      { Expr::TupleField { span: $span, lhs: Box::new($1), field: span_of_tok($3) } }
   | Expr '@' 'ID'
       { Expr::Field { span: $span, lhs: Box::new($1), field: span_of_tok($3) } }
-  | '(' Expr ')'
-      { $2 }
+  | '(' Exprs ')'
+      { exprs_to_expr($span, $2) }
+  | '[' Exprs ']'
+      { exprs_to_expr($span, $2) }
   | '{' Schedule '}'
       { Expr::BlockExpr { span: $span, body: Box::new($2) } }
   | '<' Fields '>'
@@ -73,14 +77,18 @@ Expr -> Expr
   ;
 
 Args -> Vec<Expr>
-  :               { vec![] }
-  | '[' Exprs ']' { rev($2) }
+  :                { vec![] }
+  | '[' RExprs ']' { rev($2) }
   ;
 
 Exprs -> Vec<Expr>
-  :                 { vec![] }
-  | Expr            { vec![$1] }
-  | Expr ',' Exprs  { snoc($1, $3) }
+  : RExprs  { rev($1) }
+  ;
+
+RExprs -> Vec<Expr>
+  :                  { vec![] }
+  | Expr             { vec![$1] }
+  | Expr ',' RExprs  { snoc($1, $3) }
   ;
 
 Fields -> Vec<(Span, Expr)>
@@ -180,6 +188,8 @@ pub enum Expr {
   BlockExpr   { span: Span, body: Box<OperationList> },
   Record      { span: Span, fields: Vec<(Span, Expr)> },
   SetOp       { span: Span, op: SetOp, lhs: Box<Expr>, rhs: Box<Expr> },
+  Tuple       { span: Span, exps: Vec<Expr> },
+  TupleField  { span: Span, lhs: Box<Expr>, field: Span },
 }
 
 pub enum Selector {
@@ -193,3 +203,11 @@ pub struct MacroDecl {
   pub selection_name: Span,
   pub def: Box<OperationList>,
 }
+
+fn exprs_to_expr(span: Span, mut exps: Vec<Expr>) -> Expr {
+  if exps.len() == 1 {
+    exps.pop().unwrap()
+  } else {
+    Expr::Tuple { span, exps }
+  }
+}
diff --git a/juno_scheduler/src/pm.rs b/juno_scheduler/src/pm.rs
index 5f2fa4cce02e04ca4499cbff8806356f81cc86bc..ef9ec038c12f10ffb2a025a7aa347a5ee5d02d9d 100644
--- a/juno_scheduler/src/pm.rs
+++ b/juno_scheduler/src/pm.rs
@@ -294,6 +294,9 @@ pub enum Value {
     Record {
         fields: HashMap<String, Value>,
     },
+    Tuple {
+        values: Vec<Value>,
+    },
     Everything {},
     Selection {
         selection: Vec<Value>,
@@ -371,6 +374,11 @@ impl Value {
                     "Expected code selection, found record".to_string(),
                 ));
             }
+            Value::Tuple { .. } => {
+                return Err(SchedulerError::SemanticError(
+                    "Expected code selection, found tuple".to_string(),
+                ));
+            }
             Value::Integer { .. } => {
                 return Err(SchedulerError::SemanticError(
                     "Expected code selection, found integer".to_string(),
@@ -1291,6 +1299,7 @@ fn interp_expr(
                 | Value::Integer { .. }
                 | Value::Boolean { .. }
                 | Value::String { .. }
+                | Value::Tuple { .. }
                 | Value::SetOp { .. } => Err(SchedulerError::UndefinedField(field.clone())),
                 Value::JunoFunction { func } => {
                     match pm.labels.borrow().iter().position(|s| s == field) {
@@ -1463,7 +1472,24 @@ fn interp_expr(
                 }
                 Ok((Value::Selection { selection: values }, changed))
             }
-        },
+        }
+        ScheduleExp::Tuple { exprs } => {
+            let mut vals = vec![];
+            let mut changed = false;
+            for exp in exprs {
+                let (val, change) = interp_expr(pm, exp, stringtab, env, functions)?;
+                vals.push(val);
+                changed = changed || change;
+            }
+            Ok((Value::Tuple { values: vals }, changed))
+        }
+        ScheduleExp::TupleField { lhs, field } => {
+            let (val, changed) = interp_expr(pm, lhs, stringtab, env, functions)?;
+            match val {
+                Value::Tuple { values } if *field < values.len() => Ok((vec_take(values, *field), changed)),
+                _ => Err(SchedulerError::SemanticError(format!("No field at index {}", field))),
+            }
+        }
     }
 }
 
@@ -1521,6 +1547,15 @@ fn update_value(
                 Some(Value::Record { fields: new_fields })
             }
         }
+        // For tuples, if we deleted values like we do for records this would mess up the indices
+        // which would behave very strangely. Instead if any field cannot be updated then we
+        // eliminate the entire value
+        Value::Tuple { values } => {
+            values.into_iter()
+                .map(|v| update_value(v, func_idx, juno_func_idx))
+                .collect::<Option<Vec<_>>>()
+                .map(|values| Value::Tuple { values })
+        }
         Value::JunoFunction { func } => {
             juno_func_idx[func.idx]
                 .clone()
@@ -3016,3 +3051,7 @@ where
     });
     labels
 }
+
+fn vec_take<T>(mut v: Vec<T>, index: usize) -> T {
+    v.swap_remove(index)
+}