{"record":{"id":"2879aac520144672","repo":"docusealco/docuseal","slug":"formula-infinite-loop","errorCode":null,"errorMessage":"Formula infinite loop","messagePattern":"Formula infinite loop","errorType":"validation","errorClass":"Submitters::SubmitValues::ValidationError","httpStatus":422,"severity":"error","filePath":"lib/submitters/submit_values.rb","lineNumber":239,"sourceCode":"          end\n\n        values = submission_values.merge(acc.compact_blank)\n\n        acc[field['uuid']] =\n          if field['type'] == 'text'\n            eval_text_formula_value(formula, values, submitter.submission)\n          else\n            normalized_formula = normalize_formula(formula, submitter.submission, submission_values:)\n\n            calculate_formula_value(normalized_formula, values)\n          end\n      end\n\n      computed_values.compact_blank\n    end\n\n    def normalize_formula(formula, submission, depth: 0, submission_values: nil)\n      raise ValidationError, 'Formula infinite loop' if depth > 10\n\n      formula.gsub(/{{(.*?)}}/) do |match|\n        uuid = Regexp.last_match(1)\n\n        if (nested_formula = submission.fields_uuid_index.dig(uuid, 'preferences', 'formula').presence)\n          if check_field_conditions(submission_values, submission.fields_uuid_index[uuid], submission.fields_uuid_index)\n            \"(#{normalize_formula(nested_formula, submission, depth: depth + 1, submission_values:)})\"\n          else\n            '0'\n          end\n        else\n          match\n        end\n      end\n    end\n\n    def calculate_formula_value(_formula, _values)\n      0","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/docusealco/docuseal/blob/004a22c1c88109c7ba0b567df011a8cb13894001/lib/submitters/submit_values.rb#L221-L257","documentation":"Raised by Submitters::SubmitValues.normalize_formula. Formula fields can reference other fields via {{uuid}} placeholders; when a referenced field itself has a preferences.formula, its formula is expanded recursively. A depth counter guards the expansion: recursion deeper than 10 levels means the formula graph is cyclic (or chained beyond 10 formula fields), so the calculation is aborted with 'Formula infinite loop'.","triggerScenarios":"A template formula field whose {{uuid}} placeholder points to another formula field that (directly or via a chain) points back — e.g. A = '{{B}} * 2' and B = '{{A}} + 1', or A = '{{A}}' — or a chain of more than 10 formula fields. Raised on every submit that computes formula values (build_formula_values).","commonSituations":"A template author copies a formula into a field that field references; a default_value or formula is edited to self-reference after launch, so all subsequent submissions fail; long formula-on-formula chains legitimately exceed depth 10; multi-submitter templates where merged values create an indirect cycle.","solutions":["Open the template and remove the circular reference: a formula must not directly or indirectly reference its own field","Flatten deep chains — reference plain number fields instead of stacking formula-on-formula more than 10 levels","Audit every field's preferences.formula for {{uuid}} tokens that resolve to fields which themselves carry a formula","If a two-way calculation is needed, split it into independent fields that read the same source fields"],"exampleFix":"# before (template field preferences)\n# field A (formula): \"{{B}} * 2\"\n# field B (formula): \"{{A}} + 1\"   -> cycle: A -> B -> A\n# after\n# field A (formula): \"{{qty}} * {{price}}\"\n# field B (formula): \"{{qty}} * {{price}} + {{tax}}\"   (only non-formula references)","handlingStrategy":"validation","validationCode":"formulas = submission.template_fields.to_h { |f| [f['uuid'], f.dig('preferences', 'formula').to_s] }\nrefs = formulas.transform_values { |s| s.scan(/\\{\\{(\\w+)\\}\\}/).flatten }\ncyclic = formulas.keys.any? do |uuid|\n  stack = refs[uuid].dup\n  seen = [uuid]\n  until stack.empty?\n    u = stack.pop\n    next false if u == uuid || (seen.include?(u) && stack.empty? && u == uuid)\n    break true if u == uuid\n    seen << u if formulas.key?(u)\n    stack.concat(refs[u]) if formulas.key?(u) && !seen.include?(u)\n  end == true\nend\n# reject the template before any submission if cyclic","typeGuard":null,"tryCatchPattern":"begin\n  Submitters::SubmitValues.call(submitter, params, request)\nrescue Submitters::SubmitValues::ValidationError => e\n  if e.message == 'Formula infinite loop'\n    Rollbar.warning(\"Cyclic formulas on submission #{submitter.submission_id}\") if defined?(Rollbar)\n  end\n  raise\nend","preventionTips":["Validate template formulas for cycles at template-save time, not at submission time","Prefer referencing plain input fields in formulas; avoid formula referencing formula","Add a template lint step in CI for imported/copied templates that scans {{uuid}} references"],"tags":["ruby","rails","e-signature","formula","circular-reference"],"backgroundTag":"circular-reference-detected","analyzedSha":"004a22c1c88109c7ba0b567df011a8cb13894001","analyzedAt":"2026-08-21T13:38:23.343Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}