Version: owlplanner 2026.9.15 (also reproduces on the hosted app at owlplanner.streamlit.app)
save_case writes a case file that the web UI cannot load. Uploading the TOML it produces on the Create Case page fails with a redacted TypeError; the traceback ends at ui_bridge.config_to_ui.
Reproduction
No case file needed — save_case produces the offending file itself:
import json, toml
from owlplanner.assistant import tools as owl
from owlplanner.config.ui_bridge import config_to_ui
res = owl.save_case(
names=["Alice", "Bob"], birth_dates=["1963-03-15", "1961-11-02"],
life_expectancy=[90, 87],
taxable=[100_000, 100_000], tax_deferred=[500_000, 400_000], roth=[50_000, 0],
initial_allocation=[60, 40, 0, 0], final_allocation=[60, 40, 0, 0],
output_dir="/tmp/owlrepro", case_name="repro",
)
conf = toml.load(json.loads(res)["toml_file"])
print(conf["asset_allocation"])
config_to_ui(conf)
Output:
{'interpolation_method': 'linear', 'type': 'spouses', 'generic': [[60, 40, 0, 0], [60, 40, 0, 0]]}
TypeError: 'int' object is not subscriptable
Streamlit traceback from the same file uploaded to the hosted app:
File "ui/Create_Case.py", line 100, in render_case_loader
if kz.createCaseFromFile(mystringio):
File "ui/sskeys.py", line 302, in createCaseFromFile
name, dic = owb.createCaseFromFile(strio)
File "ui/owlbridge.py", line 1938, in createCaseFromFile
mydic = config_to_ui(diconf, mylog=mylog)
File "src/owlplanner/config/ui_bridge.py", line 300, in config_to_ui
dic[f"j3_init%{k}{i}"] = int(g[0][k])
Cause
plan.setAllocationRatios documents two different shapes for generic: individual carries one [initial, final] pair per person (3 levels), while spouses carries a single household pair (2 levels), since assets are coordinated across spouses.
save_case correctly selects type = "spouses" when the allocation is household-wide, and writes the 2-level form. But config_to_ui handles both types in one branch and then indexes both as individual:
if alloc_type == "individual" or alloc_type == "spouses":
generic = aa.get("generic", DEFAULT_GENERIC_ALLOCATION)
g = generic[i] if i < len(generic) else DEFAULT_GENERIC_ALLOCATION[0]
for k in range(4):
dic[f"j3_init%{k}_{i}"] = int(g[0][k])
For spouses, generic[0] is the initial vector [60, 40, 0, 0], so g[0] is the integer 60 and g[0][k] raises.
The solver path is unaffected — owlcli run reads the same file correctly and solves to the expected objective. It is the config → UI direction only.
Possibly related: config/schema.py types the field as generic: Optional[List[List[List[int]]]], which admits only the individual shape, so a valid spouses allocation does not match the declared schema either.
Suggested fix
Branch on the type in config_to_ui, e.g. use g = generic (the household pair) when alloc_type == "spouses" and g = generic[i] when it is individual; and widen the schema annotation to accept both shapes.
Workaround
Rewriting the block to the equivalent individual form — the same pair repeated once per person — loads correctly in the UI and leaves the solver result unchanged:
[asset_allocation]
type = "individual"
generic = [ [ [60,40,0,0], [60,40,0,0] ], [ [60,40,0,0], [60,40,0,0] ] ]
Thanks for Owl — the MILP formulation and compare_to_baseline in particular have been very useful as an independent check on a separately-built planner.
Version: owlplanner 2026.9.15 (also reproduces on the hosted app at owlplanner.streamlit.app)
save_case writes a case file that the web UI cannot load. Uploading the TOML it produces on the Create Case page fails with a redacted TypeError; the traceback ends at ui_bridge.config_to_ui.
Reproduction
No case file needed — save_case produces the offending file itself:
import json, toml
from owlplanner.assistant import tools as owl
from owlplanner.config.ui_bridge import config_to_ui
res = owl.save_case(
names=["Alice", "Bob"], birth_dates=["1963-03-15", "1961-11-02"],
life_expectancy=[90, 87],
taxable=[100_000, 100_000], tax_deferred=[500_000, 400_000], roth=[50_000, 0],
initial_allocation=[60, 40, 0, 0], final_allocation=[60, 40, 0, 0],
output_dir="/tmp/owlrepro", case_name="repro",
)
conf = toml.load(json.loads(res)["toml_file"])
print(conf["asset_allocation"])
config_to_ui(conf)
Output:
{'interpolation_method': 'linear', 'type': 'spouses', 'generic': [[60, 40, 0, 0], [60, 40, 0, 0]]}
TypeError: 'int' object is not subscriptable
Streamlit traceback from the same file uploaded to the hosted app:
File "ui/Create_Case.py", line 100, in render_case_loader
if kz.createCaseFromFile(mystringio):
File "ui/sskeys.py", line 302, in createCaseFromFile
name, dic = owb.createCaseFromFile(strio)
File "ui/owlbridge.py", line 1938, in createCaseFromFile
mydic = config_to_ui(diconf, mylog=mylog)
File "src/owlplanner/config/ui_bridge.py", line 300, in config_to_ui
dic[f"j3_init%{k}{i}"] = int(g[0][k])
Cause
plan.setAllocationRatios documents two different shapes for generic: individual carries one [initial, final] pair per person (3 levels), while spouses carries a single household pair (2 levels), since assets are coordinated across spouses.
save_case correctly selects type = "spouses" when the allocation is household-wide, and writes the 2-level form. But config_to_ui handles both types in one branch and then indexes both as individual:
if alloc_type == "individual" or alloc_type == "spouses":
generic = aa.get("generic", DEFAULT_GENERIC_ALLOCATION)
g = generic[i] if i < len(generic) else DEFAULT_GENERIC_ALLOCATION[0]
for k in range(4):
dic[f"j3_init%{k}_{i}"] = int(g[0][k])
For spouses, generic[0] is the initial vector [60, 40, 0, 0], so g[0] is the integer 60 and g[0][k] raises.
The solver path is unaffected — owlcli run reads the same file correctly and solves to the expected objective. It is the config → UI direction only.
Possibly related: config/schema.py types the field as generic: Optional[List[List[List[int]]]], which admits only the individual shape, so a valid spouses allocation does not match the declared schema either.
Suggested fix
Branch on the type in config_to_ui, e.g. use g = generic (the household pair) when alloc_type == "spouses" and g = generic[i] when it is individual; and widen the schema annotation to accept both shapes.
Workaround
Rewriting the block to the equivalent individual form — the same pair repeated once per person — loads correctly in the UI and leaves the solver result unchanged:
[asset_allocation]
type = "individual"
generic = [ [ [60,40,0,0], [60,40,0,0] ], [ [60,40,0,0], [60,40,0,0] ] ]
Thanks for Owl — the MILP formulation and compare_to_baseline in particular have been very useful as an independent check on a separately-built planner.