diff --git a/CHANGES.md b/CHANGES.md
index ae0d96443d8c117d31ee5b836e6f97010f5d1ac4..23381b111691d934bcf813865db3477d076137b5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,7 @@
 # Change List
 
+## 2024-03-02
+Addition of a tactic that proves a sequent by applying a generic theorem on facts provided by the user. It can be used via `by Apply(thm).on(fact1, fact2,...)`. Replaces cases where using `Tautology` was overkill.
 ## 2024-02-05
 The "draft()" option can now be used at the start of a file to skip checking proofs of theorem outside this file during development.
 
diff --git a/lisa-sets/src/main/scala/lisa/Main.scala b/lisa-sets/src/main/scala/lisa/Main.scala
index 3d21fb8f44ee9ba2550c06194b27d654c07865d4..2ce01a39efaa24a8796d52e1887a01e6d76625de 100644
--- a/lisa-sets/src/main/scala/lisa/Main.scala
+++ b/lisa-sets/src/main/scala/lisa/Main.scala
@@ -16,6 +16,8 @@ trait Main extends BasicMain {
   export lisa.automation.Tautology
   export lisa.automation.Substitution
   export lisa.automation.Tableau
+  export lisa.automation.Apply
+  export lisa.automation.Exact
 
   knownDefs.update(emptySet, Some(emptySetAxiom))
   knownDefs.update(unorderedPair, Some(pairAxiom))
diff --git a/lisa-sets/src/main/scala/lisa/automation/Apply.scala b/lisa-sets/src/main/scala/lisa/automation/Apply.scala
new file mode 100644
index 0000000000000000000000000000000000000000..22b0bc9c1ccb5325a607ac56211592a913bf9305
--- /dev/null
+++ b/lisa-sets/src/main/scala/lisa/automation/Apply.scala
@@ -0,0 +1,254 @@
+package lisa.automation
+
+import lisa.fol.FOL.*
+import lisa.prooflib.BasicStepTactic.*
+import lisa.prooflib.ProofTacticLib.*
+import lisa.prooflib.SimpleDeducedSteps.*
+import lisa.prooflib.*
+import lisa.utils.unification.UnificationUtils.*
+import scala.util.boundary, boundary.break
+
+
+/**
+  * A tactic object that applies given arguments to a theorem by guessing its good instantiation.
+  * 
+  * '''Example usage:'''
+  *{{{
+  * val thm = have(transitive(z) |- x ∈ y ==> (y ∈ z ==> x ∈ z)) by ...
+  * val fact1 = have(a ∈ b) by ...
+  * val fact2 = have(b ∈ c) by ...
+  * have(transitive(c) |- a ∈ c) by (Apply(thm) on (fact1, fact2))
+  * }}}
+  * 
+  * where `x`, `y`, `z` are variables and `a`, `b`, `c` are constants.
+  * 
+  * In some cases, this tactic also guesses the good instantiation of the facts. This feature 
+  * however is not reliable at the moment.
+  * 
+  * @param lib the library that is being used 
+  * @param proof the ongoing proof object in which the tactic is called
+  * @param thm the reference theorem on which the arguments are applied. 
+  * 
+  * TODO: reimplement with a Horn clause solver
+  */
+class Apply(using val lib: Library, val proof: lib.Proof)(thm: proof.Fact) extends ProofTactic {
+
+  /**
+    * Converts a substitution map to a sequence of [[SubstPair]]
+    */
+  extension (subst: TermSubstitution) def toTSubstPair: Seq[SubstPair] = subst.map((k, v) => SubstPair(k, v)).toSeq
+
+  extension (subst: FormulaSubstitution) def toFSubstPair: Seq[SubstPair] = subst.map((k, v) => SubstPair(k, v)).toSeq
+
+
+  /**
+    * Converts a sequent to a normal form where implications are passed to the left and where conjuctions are split in the premises.
+    * 
+    * @param seq the sequent to be reduced in normal form
+    */
+  private def normalForm(seq: Sequent): Sequent =
+    def removeImplies(s: Sequent): Sequent = 
+      s.right.head match
+        case AppliedConnector(Implies, Seq(phi, psi)) => s.left + phi |- psi
+        case _ => s
+
+    def removeConjunctions(s: Sequent): Sequent = 
+      s.left.flatMap(f =>
+        f match
+          case AppliedConnector(And, Seq(phi, psi)) => Set(phi, psi)
+          case _ => Set(f)
+      ) |- s.right
+
+    removeConjunctions(removeImplies(seq))
+
+  /**
+    * Search the premises of a sequent to find one that is matched by a given formula.
+    * Performing the resulting substitution inside this premise gives the formula passed as argument. 
+    *
+    * @param seq the sequent whose premises are references
+    * @param f the formula to match
+    * @param takenTVars any term variable in the template which cannot be substituted, i.e., treated as constant
+    * @param takenFVars any formula variable in the template which cannot be substituted, i.e., treated as constant
+    * @return a variable assignment such that substituting the variables of seq makes f appear in one of its premises. If no such substitution exists return None.
+    */
+  private def searchPremises(seq: Sequent, f: Formula, takenTVars: Iterable[Variable] = Iterable.empty, takenFVars: Iterable[VariableFormula] = Iterable.empty): Option[(FormulaSubstitution, TermSubstitution)] =
+    seq.left.foldLeft[Option[(FormulaSubstitution, TermSubstitution)]](None)((assignment, cclPrem) =>
+      assignment match
+        case None => matchFormula(f, cclPrem, takenTVars, takenFVars)
+        case _ => assignment
+    )
+
+  /**
+    * Search the premises of a sequent to find one that matches a given formula.
+    * Performing the resulting substitution inside this formula gives the premise. 
+    *
+    * @param seq the sequent whose premises are being searched
+    * @param f the reference formula
+    * @param takenTVars any term variable in the template which cannot be substituted, i.e., treated as constant
+    * @param takenFVars any formula variable in the template which cannot be substituted, i.e., treated as constant
+    * @return a variable assignment such that substituting the variables of f makes f appear in one of seq's premises. If no such substitution exists return None.
+    */
+  private def searchPremisesRev(seq: Sequent, f: Formula, takenTVars: Iterable[Variable] = Iterable.empty, takenFVars: Iterable[VariableFormula] = Iterable.empty): Option[(FormulaSubstitution, TermSubstitution)] =
+    seq.left.foldLeft[Option[(FormulaSubstitution, TermSubstitution)]](None)((assignment, cclPrem) =>
+      assignment match
+        case None => matchFormula(cclPrem, f, takenTVars, takenFVars)
+        case _ => assignment
+    )
+
+  /**
+    * Substitute the variables of a fact with given assignments.
+    *
+    * @param proof the ongoing proof object in which the substitution happens
+    * @param fact the fact whose variables are being substituted
+    * @param fSubst the assignment for formula variables
+    * @param tSubst the assignment for term variables
+    */
+  private def substitute(using _proof: lib.Proof)(fact: _proof.Fact, fSubst: FormulaSubstitution, tSubst: TermSubstitution): _proof.Fact =
+    fact.of(fSubst.toFSubstPair: _*).of(tSubst.toTSubstPair: _*)
+
+
+  /**
+    * Applies on method with a sequence as an argument instead of a varargs.
+    */
+  infix def onSeq(premises: Seq[proof.Fact])(bot: Sequent): proof.ProofTacticJudgement = on(premises : _*)(bot)
+
+  /**
+    * Executes the tactic. See class description for use cases.
+    *
+    * @param premises the facts that are applied to the theorem
+    * @param bot the sequent the user want to prove
+    */
+  infix def on(premises: proof.Fact*)(bot: Sequent): proof.ProofTacticJudgement =
+
+    boundary:
+      TacticSubproof { sp ?=>
+
+        // STEP 1: Convert the given theorem, facts and sequent to their normal forms
+        val botNormalized: Sequent = normalForm(bot)
+        val thmNormalized: sp.Fact = lib.have(normalForm(thm.statement)) by Restate.from(thm)
+        val premisesNormalized = premises.map(p => lib.have(normalForm(p.statement)) by Restate.from(p))
+        
+        // STEP 2: Unify the conclusion of the sequent the user want to prove and the conclusion
+        val botCcl: Formula = botNormalized.right.head
+        val thmCcl: Formula = thmNormalized.statement.right.head
+
+        matchFormula(botCcl, thmCcl) match
+          case Some((formulaCclAssignment, termCclAssignment)) =>
+
+            //Unification succeeded, subtitution can be performed
+            val thmAfterCclUnification: sp.Fact = substitute(thmNormalized, formulaCclAssignment, termCclAssignment)
+
+            // STEP 3: Process each fact
+            val thmAfterPrems: sp.Fact = {
+
+              premisesNormalized.foldLeft(lib.have(thmAfterCclUnification))((updatedThm, premNormalized) => {
+
+                
+                val premCcl: Formula = premNormalized.statement.right.head
+
+                // STEP 3.1: Unify the conclusion of the fact with a premise of the theorem
+                // Three possibilities:
+                //   - the unification succeeded and the variables in the theorem are subtituted to match the conclusion of the fact;
+                //   - if the unification did not succeeded, try the unification in the other direction, i.e. try to specify the fact
+                //     instead of the theorem. If this works, make the susbtitution inside the fact;
+                //   - both unifications do not succeed and the fact is dropped.
+                //     TODO: add a warning when the fact is dropped
+                val conclusionsUnification: Option[(sp.Fact, sp.Fact)] = 
+                  searchPremises(updatedThm.statement, premCcl) match
+                    case Some((fSubstAfterPrem, tSubstAfterPrem)) => Some((substitute(updatedThm, fSubstAfterPrem, tSubstAfterPrem), premNormalized))
+                    case None => 
+                      searchPremisesRev(updatedThm.statement, premCcl) match
+                        case Some((fSubstAfterPrem, tSubstAfterPrem)) => Some((updatedThm, substitute(premNormalized, fSubstAfterPrem, tSubstAfterPrem)))
+                        case None =>  None
+
+                
+
+
+                conclusionsUnification match 
+                  case Some(updatedThmAfterCclUnification, premAfterCclUnification) =>
+
+                    // STEP 3.2: For each premise of the fact:
+                    //   - check if it is in the sequent the user want to prove. If yes, add it to the preconditions of the theorem
+                    //     using weakening;
+                    //   - else if it matches one of the premises of the theorem specify the theorem by using the appropriate sustitution.
+                    //     When performing this operation, the conclusion of the fact must be temporarily removed from the premises of the theorem
+                    //     to avoid buggy situations in case the fact is of the form p |- p;
+                    //   - else add the premise to the premises of the theorem using weakening.
+                    val premCclAfterCclUnification: Formula = premAfterCclUnification.statement.right.head
+
+                    val updatedThmAfterWeakening: sp.Fact = 
+                      premAfterCclUnification.statement.left.foldLeft(updatedThmAfterCclUnification)((updatedThmDuringWeakening, premPrem) => {
+                        if botNormalized.left.contains(premPrem) then 
+                          lib.have(updatedThmDuringWeakening.statement +<< premPrem) by Weakening(updatedThmDuringWeakening)
+                        else 
+                          searchPremises(updatedThmDuringWeakening.statement -<? premCclAfterCclUnification, premPrem) match
+                            case Some((fSubstDuringWeakening, tSubstDuringWeakening)) =>
+                              substitute(updatedThmDuringWeakening, fSubstDuringWeakening, tSubstDuringWeakening)
+                            case None =>
+                              lib.have(updatedThmDuringWeakening.statement +<< premPrem) by Weakening(updatedThmDuringWeakening)
+                      })
+
+
+                    // STEP 3.3 Use cut to apply the conclusion of the fact to the theorem
+                    // TODO: maybe throw a warning when the fact cannot be applied instead of making the proof crash
+                    val cutStep: sp.ProofTacticJudgement = Cut(premAfterCclUnification, updatedThmAfterWeakening)(updatedThmAfterWeakening.statement -<? premCclAfterCclUnification)
+
+                    if cutStep.isValid then
+                      lib.have(cutStep)
+                    else
+                      break(proof.InvalidProofTactic(s"The following argument could not be applied to the theorem\n${premAfterCclUnification.statement}"))
+
+                    
+
+                  // STEP 3.1 failed: the fact is dropped
+                  case None => updatedThm
+                    
+              })
+            }
+            
+            // STEP 4: If some cases, after all these operations, the theorem can remain too general to prove the sequent.
+            //         To avoid such situation, perform a last unification between the premises of the sequent and those 
+            //         of the theorem.
+            val thmAfterLastUnification: sp.Fact = botNormalized.left.foldLeft(thmAfterPrems)((updatedThm, premBot) => {
+              searchPremises(updatedThm.statement, premBot) match 
+                case Some(fSubst, tSubst) => substitute(updatedThm, fSubst, tSubst)
+                case None => updatedThm
+            })
+
+            // STEP 5: Prove the sequent using the theorem obtained with the previous steps. Weakening is necessary in case 
+            //         additional preconditions that do not appear in the theorem are present in the sequent.
+            val finalStep: sp.ProofTacticJudgement = Weakening(thmAfterLastUnification)(bot)
+            if finalStep.isValid then
+              lib.have(finalStep)
+            else
+              break(proof.InvalidProofTactic(s"The given theorem could not prove the sequent\n. Deduced theorem: ${thmAfterLastUnification.statement}"))
+
+          // STEP 2 failed
+          case None => break(proof.InvalidProofTactic(s"The conclusion of the goal and the theorem could not be unified."))
+
+      }
+
+}
+
+/**
+  * Helper that creates an [[Apply]] object.
+  */
+object Apply {
+  def apply(using lib: Library, _proof: lib.Proof)(thm: _proof.Fact): Apply{val proof: _proof.type} = (new Apply(using lib, _proof)(thm)).asInstanceOf
+}
+
+/**
+  * Helper to call the Apply tactic without arguments. When no facts are provided by the user, they can call
+  * {{{
+  *   Exact(thm)
+  * }}} 
+  * as a shortcut for 
+  * {{{
+  *   Apply(thm).on()
+  * }}}
+  * 
+  * TODO: find an appropriate name for this tactic or replace it with Apply
+  */
+object Exact {
+  def apply(using lib: Library, _proof: lib.Proof)(thm: _proof.Fact)(bot: Sequent): _proof.ProofTacticJudgement = Apply(thm).on()(bot)
+}
\ No newline at end of file