Skip to content

v15 Stable Child Tables API (Python & JS)

In Frappe Framework v15, Child Tables are embedded sub-documents (DocTypes with istable: 1) linked directly to a parent document via parent, parentfield, and parenttype schema columns.


1. Server-Side Child Table API (Python)

Appending & Inserting Child Rows (doc.append)

python
import frappe

doc = frappe.get_doc("Sales Invoice", "SINV-2026-00001")

# Append new row to 'items' child table
new_row = doc.append("items", {
    "item_code": "LAPTOP-DELL-XPS",
    "qty": 1,
    "rate": 1200.00,
    "amount": 1200.00
})

# Access newly assigned child row properties
print(new_row.name)  # Auto-generated row primary key (e.g. 'row-0001')
doc.save()

Iterating, Updating & Removing Child Rows

python
doc = frappe.get_doc("Task", "TASK-00001")

# 1. Iterate child table rows
total_estimated_hours = 0.0
for row in doc.get("assignees"):
    total_estimated_hours += row.hours
    if row.user == "inactive_user@example.com":
        # Modify child row attribute
        row.status = "Inactive"

# 2. Filter & remove child table rows matching condition
doc.assignees = [row for row in doc.assignees if row.status != "Inactive"]

# Save parent document
doc.save()

2. Client-Side Child Table API (JavaScript)

Binding Child Table Field Triggers

Use frappe.ui.form.on(child_doctype_name, handlers) to bind events to child table fields:

javascript
// Target the Child DocType name ("Sales Invoice Item"), NOT the table fieldname!
frappe.ui.form.on("Sales Invoice Item", {
    item_code(frm, cdt, cdn) {
        // cdt: Child DocType name string ("Sales Invoice Item")
        // cdn: Child Document row name string ("row-0001")
        let row = frappe.get_doc(cdt, cdn);
        
        if (row.item_code) {
            frappe.db.get_value("Item", row.item_code, "standard_rate", (r) => {
                if (r && r.standard_rate) {
                    frappe.model.set_value(cdt, cdn, "rate", r.standard_rate);
                    frappe.model.set_value(cdt, cdn, "amount", r.standard_rate * row.qty);
                }
            });
        }
    },
    qty(frm, cdt, cdn) {
        let row = frappe.get_doc(cdt, cdn);
        frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate);
    },
    items_remove(frm, cdt, cdn) {
        // Triggered when a child row is deleted from grid
        frm.trigger("calculate_totals");
    }
});

Adding, Clearing & Editing Child Rows in Desk Form

javascript
frappe.ui.form.on("Sales Invoice", {
    add_default_service_fee(frm) {
        // 1. Add new child row programmatically
        let child_row = frm.add_child("items");
        child_row.item_code = "SERVICE-FEE";
        child_row.qty = 1;
        child_row.rate = 50.00;
        
        // 2. Refresh table DOM grid view
        frm.refresh_field("items");
    },
    clear_all_items(frm) {
        // Clear all rows from 'items' table
        frm.clear_table("items");
        frm.refresh_field("items");
    }
});

javascript
frappe.ui.form.on("Sales Invoice", {
    refresh(frm) {
        // Apply filter to 'item_code' field inside 'items' child table grid
        frm.set_query("item_code", "items", function(doc, cdt, cdn) {
            return {
                filters: {
                    is_sales_item: 1,
                    disabled: 0
                }
            };
        });
    }
});

3. Desk Grid UI API Reference

javascript
// Access underlying Grid object
let grid = frm.get_field("items").grid;

// 1. Toggle grid row buttons (Add, Delete)
grid.cannot_add_rows = true;
grid.refresh();

// 2. Make specific column read-only dynamically
grid.get_field("rate").df.read_only = 1;
grid.refresh();

Frappe Framework v15 Complete Technical Reference & Handbook.